langchain-modular-rag-gary 0.1.0__tar.gz

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.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-modular-rag-gary
3
+ Version: 0.1.0
4
+ Summary: A modular RAG retrieval class based on LangChain and Chroma
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: chromadb
8
+ Requires-Dist: langchain-chroma
9
+ Requires-Dist: langchain-community
10
+ Requires-Dist: langchain-core
11
+ Requires-Dist: langchain-openai
12
+ Requires-Dist: langchain-classic
13
+ Requires-Dist: jieba
14
+ Requires-Dist: rank-bm25
15
+ Requires-Dist: python-dotenv
16
+ Requires-Dist: dashscope
17
+ Provides-Extra: publish
18
+ Requires-Dist: build; extra == "publish"
19
+ Requires-Dist: twine; extra == "publish"
20
+
21
+ # langchain-modular-rag-gary
22
+
23
+ 提供一个 `ModularRAG` 类,用于连接已有的 Chroma 知识库并执行在线检索。
24
+
25
+ Python 3.11 及以上。
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ pip install langchain-modular-rag-gary
31
+ ```
32
+
33
+ ## 使用
34
+
35
+ ```python
36
+ from langchain_modular_rag_gary import ModularRAG
37
+
38
+ rag = ModularRAG({
39
+ "persist_directory": "./chroma_db",
40
+ "collection_name": "study",
41
+ "embedding_model": "text-embedding-v4",
42
+ "top_k": 5,
43
+ "recall_top_k": 20,
44
+ "pre_retrieval": {
45
+ "enabled": False,
46
+ "rewrite_query": False,
47
+ "hyde": False,
48
+ "model": "qwen-plus",
49
+ },
50
+ "hybrid_retrieval": {
51
+ "enabled": True,
52
+ "weights": [0.5, 0.5],
53
+ },
54
+ "post_retrieval": {
55
+ "enabled": True,
56
+ "deduplicate": True,
57
+ "rerank": False,
58
+ "rerank_model": "qwen3-rerank",
59
+ "similarity_filter": False,
60
+ "similarity_threshold": 0.4,
61
+ "compress_context": False,
62
+ "llm_model": "qwen-plus",
63
+ "reorder": False,
64
+ },
65
+ })
66
+
67
+ results = rag.search("公司打卡时间?")
68
+ for document in results:
69
+ print(document.metadata)
70
+ print(document.page_content)
71
+ ```
72
+
73
+ ## 环境变量
74
+
75
+ 需要配置通义 API Key:
76
+
77
+ ```bash
78
+ export DASHSCOPE_API_KEY="你的 API Key"
79
+ 或
80
+ setx DASHSCOPE_API_KEY="你的 API Key"
81
+ ```
82
+
83
+ `persist_directory` 指向已经完成入库的 Chroma 数据库。这个包当前只提供在线检索,不包含你的个人知识库数据,也不会自动创建或导入知识库。
@@ -0,0 +1,63 @@
1
+ # langchain-modular-rag-gary
2
+
3
+ 提供一个 `ModularRAG` 类,用于连接已有的 Chroma 知识库并执行在线检索。
4
+
5
+ Python 3.11 及以上。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pip install langchain-modular-rag-gary
11
+ ```
12
+
13
+ ## 使用
14
+
15
+ ```python
16
+ from langchain_modular_rag_gary import ModularRAG
17
+
18
+ rag = ModularRAG({
19
+ "persist_directory": "./chroma_db",
20
+ "collection_name": "study",
21
+ "embedding_model": "text-embedding-v4",
22
+ "top_k": 5,
23
+ "recall_top_k": 20,
24
+ "pre_retrieval": {
25
+ "enabled": False,
26
+ "rewrite_query": False,
27
+ "hyde": False,
28
+ "model": "qwen-plus",
29
+ },
30
+ "hybrid_retrieval": {
31
+ "enabled": True,
32
+ "weights": [0.5, 0.5],
33
+ },
34
+ "post_retrieval": {
35
+ "enabled": True,
36
+ "deduplicate": True,
37
+ "rerank": False,
38
+ "rerank_model": "qwen3-rerank",
39
+ "similarity_filter": False,
40
+ "similarity_threshold": 0.4,
41
+ "compress_context": False,
42
+ "llm_model": "qwen-plus",
43
+ "reorder": False,
44
+ },
45
+ })
46
+
47
+ results = rag.search("公司打卡时间?")
48
+ for document in results:
49
+ print(document.metadata)
50
+ print(document.page_content)
51
+ ```
52
+
53
+ ## 环境变量
54
+
55
+ 需要配置通义 API Key:
56
+
57
+ ```bash
58
+ export DASHSCOPE_API_KEY="你的 API Key"
59
+ 或
60
+ setx DASHSCOPE_API_KEY="你的 API Key"
61
+ ```
62
+
63
+ `persist_directory` 指向已经完成入库的 Chroma 数据库。这个包当前只提供在线检索,不包含你的个人知识库数据,也不会自动创建或导入知识库。
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-modular-rag-gary
3
+ Version: 0.1.0
4
+ Summary: A modular RAG retrieval class based on LangChain and Chroma
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: chromadb
8
+ Requires-Dist: langchain-chroma
9
+ Requires-Dist: langchain-community
10
+ Requires-Dist: langchain-core
11
+ Requires-Dist: langchain-openai
12
+ Requires-Dist: langchain-classic
13
+ Requires-Dist: jieba
14
+ Requires-Dist: rank-bm25
15
+ Requires-Dist: python-dotenv
16
+ Requires-Dist: dashscope
17
+ Provides-Extra: publish
18
+ Requires-Dist: build; extra == "publish"
19
+ Requires-Dist: twine; extra == "publish"
20
+
21
+ # langchain-modular-rag-gary
22
+
23
+ 提供一个 `ModularRAG` 类,用于连接已有的 Chroma 知识库并执行在线检索。
24
+
25
+ Python 3.11 及以上。
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ pip install langchain-modular-rag-gary
31
+ ```
32
+
33
+ ## 使用
34
+
35
+ ```python
36
+ from langchain_modular_rag_gary import ModularRAG
37
+
38
+ rag = ModularRAG({
39
+ "persist_directory": "./chroma_db",
40
+ "collection_name": "study",
41
+ "embedding_model": "text-embedding-v4",
42
+ "top_k": 5,
43
+ "recall_top_k": 20,
44
+ "pre_retrieval": {
45
+ "enabled": False,
46
+ "rewrite_query": False,
47
+ "hyde": False,
48
+ "model": "qwen-plus",
49
+ },
50
+ "hybrid_retrieval": {
51
+ "enabled": True,
52
+ "weights": [0.5, 0.5],
53
+ },
54
+ "post_retrieval": {
55
+ "enabled": True,
56
+ "deduplicate": True,
57
+ "rerank": False,
58
+ "rerank_model": "qwen3-rerank",
59
+ "similarity_filter": False,
60
+ "similarity_threshold": 0.4,
61
+ "compress_context": False,
62
+ "llm_model": "qwen-plus",
63
+ "reorder": False,
64
+ },
65
+ })
66
+
67
+ results = rag.search("公司打卡时间?")
68
+ for document in results:
69
+ print(document.metadata)
70
+ print(document.page_content)
71
+ ```
72
+
73
+ ## 环境变量
74
+
75
+ 需要配置通义 API Key:
76
+
77
+ ```bash
78
+ export DASHSCOPE_API_KEY="你的 API Key"
79
+ 或
80
+ setx DASHSCOPE_API_KEY="你的 API Key"
81
+ ```
82
+
83
+ `persist_directory` 指向已经完成入库的 Chroma 数据库。这个包当前只提供在线检索,不包含你的个人知识库数据,也不会自动创建或导入知识库。
@@ -0,0 +1,8 @@
1
+ README.md
2
+ langchain_modular_rag_gary.py
3
+ pyproject.toml
4
+ langchain_modular_rag_gary.egg-info/PKG-INFO
5
+ langchain_modular_rag_gary.egg-info/SOURCES.txt
6
+ langchain_modular_rag_gary.egg-info/dependency_links.txt
7
+ langchain_modular_rag_gary.egg-info/requires.txt
8
+ langchain_modular_rag_gary.egg-info/top_level.txt
@@ -0,0 +1,14 @@
1
+ chromadb
2
+ langchain-chroma
3
+ langchain-community
4
+ langchain-core
5
+ langchain-openai
6
+ langchain-classic
7
+ jieba
8
+ rank-bm25
9
+ python-dotenv
10
+ dashscope
11
+
12
+ [publish]
13
+ build
14
+ twine
@@ -0,0 +1 @@
1
+ langchain_modular_rag_gary
@@ -0,0 +1,308 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ @Project :book
4
+ @File :langchain_modular_rag_gary.py.py
5
+ @Author :kangyu
6
+ @Date :2026/11/7 11:38
7
+ """
8
+
9
+
10
+ class ModularRAG:
11
+ def __init__(self, config: dict):
12
+ import os
13
+ from pathlib import Path
14
+ from dotenv import load_dotenv
15
+ from langchain_chroma import Chroma
16
+ from langchain_community.embeddings import DashScopeEmbeddings
17
+
18
+ load_dotenv()
19
+ self.config = config
20
+ directory = config.get("persist_directory", "./chroma_db")
21
+ if not (Path(directory).expanduser() / "chroma.sqlite3").is_file():
22
+ raise FileNotFoundError(f"知识库不存在:{directory}")
23
+ self.embeddings = DashScopeEmbeddings(
24
+ model=config.get("embedding_model", "text-embedding-v4"),
25
+ dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"),
26
+ )
27
+ self.db = Chroma(
28
+ collection_name=config.get("collection_name", "study"),
29
+ persist_directory=str(Path(directory).expanduser()),
30
+ embedding_function=self.embeddings,
31
+ create_collection_if_not_exists=False,
32
+ )
33
+
34
+ def search(self, question: str, top_k=None, filters=None):
35
+ from langchain_core.documents import Document
36
+
37
+ if not question.strip():
38
+ raise ValueError("问题不能为空")
39
+ limit = self.config.get("top_k", 5) if top_k is None else top_k
40
+ if not isinstance(limit, int) or limit <= 0:
41
+ raise ValueError("top_k 必须是正整数")
42
+ count = max(self.config.get("recall_top_k", 20), limit)
43
+ pre = self.config.get("pre_retrieval", {})
44
+ hybrid = self.config.get("hybrid_retrieval", {})
45
+ post = self.config.get("post_retrieval", {})
46
+
47
+ query = question
48
+ if pre.get("enabled", False):
49
+ query = self.normalize_query(query)
50
+ if pre.get("rewrite_query", False):
51
+ query = self.rewrite_query(query, model=pre.get("model", "qwen-plus"))
52
+ keyword_query = query
53
+ dense_query = query
54
+ if pre.get("enabled", False) and pre.get("hyde", False):
55
+ dense_query = self.generate_hyde(query, model=pre.get("model", "qwen-plus"))
56
+
57
+ conditions = []
58
+ for condition in (self.config.get("filters"), filters):
59
+ if condition:
60
+ conditions.extend({key: value} for key, value in condition.items())
61
+ where = None
62
+ if len(conditions) == 1:
63
+ where = conditions[0]
64
+ elif len(conditions) > 1:
65
+ where = {"$and": conditions}
66
+
67
+ if hybrid.get("enabled", False):
68
+ import jieba
69
+ from langchain_community.retrievers import BM25Retriever
70
+ from langchain_core.runnables import RunnableLambda
71
+
72
+ stored = self.db.get(where=where, include=["documents", "metadatas"])
73
+ corpus = []
74
+ for index, text in enumerate(stored["documents"] or []):
75
+ corpus.append(Document(
76
+ id=stored["ids"][index],
77
+ page_content=text,
78
+ metadata=stored["metadatas"][index] or {},
79
+ ))
80
+ if not corpus:
81
+ return []
82
+ vector_retriever = self.db.as_retriever(
83
+ search_type="similarity",
84
+ search_kwargs={"k": count, "filter": where},
85
+ )
86
+ if dense_query != keyword_query:
87
+ vector_retriever = (
88
+ RunnableLambda(lambda incoming_query: dense_query) | vector_retriever
89
+ )
90
+ bm25_retriever = BM25Retriever.from_documents(
91
+ corpus,
92
+ preprocess_func=lambda text: list(jieba.cut(text)),
93
+ k=count,
94
+ )
95
+ documents = self.rrf_fusion(
96
+ keyword_query, vector_retriever, bm25_retriever,
97
+ weights=hybrid.get("weights", [0.5, 0.5]),
98
+ )
99
+ else:
100
+ documents = self.vector_search(dense_query, self.db, count, where)
101
+
102
+ if post.get("enabled", False):
103
+ if post.get("deduplicate", True):
104
+ documents = self.deduplicate(documents)
105
+ if post.get("rerank", False):
106
+ documents = self.rerank_documents(
107
+ question, documents, top_k=len(documents),
108
+ model=post.get("rerank_model", "qwen3-rerank"),
109
+ )
110
+ if post.get("similarity_filter", False):
111
+ documents = self.similarity_filter(
112
+ question, documents, self.embeddings,
113
+ threshold=post.get("similarity_threshold", 0.4),
114
+ )
115
+
116
+ documents = documents[:limit]
117
+ for rank, document in enumerate(documents, start=1):
118
+ document.metadata["retrieval_rank"] = rank
119
+
120
+ if post.get("enabled", False):
121
+ if post.get("compress_context", False):
122
+ documents = self.compress_context(
123
+ question, documents, model=post.get("llm_model", "qwen-plus")
124
+ )
125
+ if post.get("reorder", False):
126
+ documents = self.reorder_documents(documents)
127
+ for position, document in enumerate(documents, start=1):
128
+ document.metadata["context_position"] = position
129
+ return documents
130
+
131
+ @staticmethod
132
+ # ==================== 1. 归一化查询词:移除所有空格并首尾去空格 ====================
133
+ def normalize_query(question: str):
134
+ import re
135
+ return re.sub(r"\s+", " ", question).strip() # 移除所有空格并首尾去空格
136
+
137
+ @staticmethod
138
+ # ==================== 2. 改写查询词:根据模型提示改写为适合知识库检索的独立查询 ====================
139
+ def rewrite_query(question: str, model="qwen-plus"):
140
+ import os
141
+ from dotenv import load_dotenv
142
+ from langchain_openai import ChatOpenAI
143
+
144
+ load_dotenv()
145
+ llm = ChatOpenAI(
146
+ model=model,
147
+ api_key=os.getenv("DASHSCOPE_API_KEY"),
148
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
149
+ temperature=0,
150
+ )
151
+ response = llm.invoke([
152
+ ("system", "把问题改写为适合知识库检索的独立查询。保留原意,不编造缺失上下文,只输出查询。"),
153
+ ("human", question),
154
+ ])
155
+ return response.content.strip()
156
+
157
+ @staticmethod
158
+ # ==================== 3. 生成假设性答案:根据模型提示生成约100字的假设性答案 ====================
159
+ def generate_hyde(question: str, model="qwen-plus"):
160
+ import os
161
+ from dotenv import load_dotenv
162
+ from langchain_openai import ChatOpenAI
163
+
164
+ load_dotenv()
165
+ llm = ChatOpenAI(
166
+ model=model,
167
+ api_key=os.getenv("DASHSCOPE_API_KEY"),
168
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
169
+ temperature=0,
170
+ )
171
+ response = llm.invoke([
172
+ ("system", "为问题生成约100字的假设性答案,用于语义检索。只输出假设答案,不将其视为事实证据。"),
173
+ ("human", question),
174
+ ])
175
+ return response.content.strip()
176
+
177
+ @staticmethod
178
+ # ==================== 4. 稠密:向量检索召回文档 ====================
179
+ def vector_search(question: str, db, top_k=5, filters=None):
180
+ retriever = db.as_retriever(
181
+ search_type="similarity",
182
+ search_kwargs={"k": top_k, "filter": filters},
183
+ )
184
+ documents = retriever.invoke(question)
185
+ for document in documents:
186
+ document.metadata["chunk_id"] = document.id
187
+ return documents
188
+
189
+ @staticmethod
190
+ # ==================== 5. 稀疏:BM25检索召回文档 ====================
191
+ def bm25_search(question: str, documents, top_k=5):
192
+ import jieba
193
+ from langchain_community.retrievers import BM25Retriever
194
+
195
+ if not documents:
196
+ return []
197
+ bm25_retriever = BM25Retriever.from_documents(
198
+ documents,
199
+ preprocess_func=lambda text: list(jieba.cut(text)),
200
+ k=top_k,
201
+ )
202
+ return bm25_retriever.invoke(question)
203
+
204
+ @staticmethod
205
+ # ==================== 6. RRF融合召回文档 ====================
206
+ def rrf_fusion(question: str, vector_retriever, bm25_retriever, weights=None, rrf_k=60):
207
+ from langchain_classic.retrievers import EnsembleRetriever
208
+
209
+ retriever = EnsembleRetriever(
210
+ retrievers=[vector_retriever, bm25_retriever],
211
+ weights=weights or [0.5, 0.5],
212
+ c=rrf_k,
213
+ )
214
+ return retriever.invoke(question)
215
+
216
+ @staticmethod
217
+ # ==================== 7. 去重:根据文档内容去重 ====================
218
+ def deduplicate(documents):
219
+ seen = set()
220
+ results = []
221
+ for document in documents:
222
+ if document.page_content not in seen:
223
+ seen.add(document.page_content)
224
+ results.append(document)
225
+ return results
226
+
227
+ from langchain_core.documents import Document
228
+
229
+ @staticmethod
230
+ # ==================== 8. 重排:根据模型提示重排召回文档 ====================
231
+ def rerank_documents(question: str, documents, top_k=5, model="qwen3-rerank"):
232
+ import os
233
+ from dotenv import load_dotenv
234
+ from langchain_community.document_compressors import DashScopeRerank
235
+
236
+ if not documents:
237
+ return []
238
+ load_dotenv()
239
+ reranker = DashScopeRerank(
240
+ model=model,
241
+ dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"),
242
+ top_n=top_k,
243
+ )
244
+ return list(reranker.compress_documents(documents, question))
245
+
246
+ @staticmethod
247
+ # ==================== 9. 筛选:根据文档相似度筛选出相关文档 ====================
248
+ def similarity_filter(question: str, documents, embeddings, threshold=0.4):
249
+ import math
250
+
251
+ if not documents:
252
+ return []
253
+ query_vector = embeddings.embed_query(question)
254
+ vectors = embeddings.embed_documents([
255
+ document.page_content for document in documents
256
+ ])
257
+ query_norm = math.sqrt(sum(value * value for value in query_vector))
258
+
259
+ results = []
260
+ for document, vector in zip(documents, vectors):
261
+ vector_norm = math.sqrt(sum(value * value for value in vector))
262
+ if query_norm == 0 or vector_norm == 0:
263
+ continue
264
+ similarity = sum(
265
+ query_value * value for query_value, value in zip(query_vector, vector)
266
+ ) / (query_norm * vector_norm)
267
+ if similarity >= threshold:
268
+ result = document.model_copy(deep=True)
269
+ result.metadata["cosine_similarity"] = similarity
270
+ results.append(result)
271
+ return results
272
+
273
+ @staticmethod
274
+ # ==================== 10. 压缩:根据模型提示压缩文档上下文 ====================
275
+ def compress_context(question: str, documents, model="qwen-plus"):
276
+ import os
277
+ from dotenv import load_dotenv
278
+ from langchain_openai import ChatOpenAI
279
+ from langchain_classic.retrievers.document_compressors import LLMChainExtractor
280
+
281
+ if not documents:
282
+ return []
283
+ load_dotenv()
284
+ llm = ChatOpenAI(
285
+ model=model,
286
+ api_key=os.getenv("DASHSCOPE_API_KEY"),
287
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
288
+ temperature=0,
289
+ )
290
+ compressor = LLMChainExtractor.from_llm(llm)
291
+ results = []
292
+ for document in documents:
293
+ is_table = (
294
+ document.metadata.get("content_type") == "table"
295
+ or "<table" in document.page_content.lower()
296
+ )
297
+ if is_table:
298
+ results.append(document)
299
+ else:
300
+ results.extend(compressor.compress_documents([document], question))
301
+ return results
302
+
303
+ @staticmethod
304
+ # ==================== 11. 压缩:根据模型提示压缩文档上下文 ====================
305
+ def reorder_documents(documents):
306
+ from langchain_community.document_transformers import LongContextReorder
307
+
308
+ return list(LongContextReorder().transform_documents(documents))
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "langchain-modular-rag-gary"
7
+ version = "0.1.0"
8
+ description = "A modular RAG retrieval class based on LangChain and Chroma"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "chromadb",
13
+ "langchain-chroma",
14
+ "langchain-community",
15
+ "langchain-core",
16
+ "langchain-openai",
17
+ "langchain-classic",
18
+ "jieba",
19
+ "rank-bm25",
20
+ "python-dotenv",
21
+ "dashscope",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ publish = ["build", "twine"]
26
+
27
+ [tool.setuptools]
28
+ py-modules = ["langchain_modular_rag_gary"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+