From fd87f9797d0fae1a314cd11bab38d1bfd654aaaa Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:37:02 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .env | 12 ++ LICENSE | 201 ++++++++++++++++++++++++++++++++++ README.md | 102 +++++++++++++++++ README.wehub.md | 7 ++ config.py | 36 ++++++ file_process.py | 45 ++++++++ main.py | 18 +++ milvus_vector.py | 37 +++++++ protocol/__init__.py | 0 protocol/mode.py | 12 ++ protocol/prompts.py | 13 +++ requirements.txt | 153 ++++++++++++++++++++++++++ server.py | 141 ++++++++++++++++++++++++ static/css/qa_style.css | 151 +++++++++++++++++++++++++ static/images/bot.jpg | Bin 0 -> 4464 bytes static/images/user.jpg | Bin 0 -> 43470 bytes static/js/kb_manager.js | 73 ++++++++++++ static/js/qa_script.js | 97 ++++++++++++++++ templates/qa.html | 29 +++++ text_utils/__init__.py | 0 text_utils/text_embeddings.py | 30 +++++ text_utils/text_split.py | 76 +++++++++++++ 22 files changed, 1233 insertions(+) create mode 100644 .env create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 config.py create mode 100644 file_process.py create mode 100644 main.py create mode 100644 milvus_vector.py create mode 100644 protocol/__init__.py create mode 100644 protocol/mode.py create mode 100644 protocol/prompts.py create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 static/css/qa_style.css create mode 100644 static/images/bot.jpg create mode 100644 static/images/user.jpg create mode 100644 static/js/kb_manager.js create mode 100644 static/js/qa_script.js create mode 100644 templates/qa.html create mode 100644 text_utils/__init__.py create mode 100644 text_utils/text_embeddings.py create mode 100644 text_utils/text_split.py diff --git a/.env b/.env new file mode 100644 index 0000000..1b41a00 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +fastapi_host=localhost +fastapi_port=8000 + +milvus_host=localhost +milvus_port=19530 +milvus_collection_name=knowledge_collection + +openai_base_url=[YOUR_BASE_URL] +openai_api_key=[YOUR_API_KEY] +openai_llm_model_name=gpt-4o-mini + +text_embeddings_model_path=jinaai/jina-embeddings-v3 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..49ee88a --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Knowledge_QA_RAG + +## Overview +本系统是一种基于 RAG 的知识库问答系统简单示例,采用前后端分离的架构设计,融合了多种技术和方法。具体详情可参考:[https://blog.csdn.net/weixin_47936614/article/details/143932997](https://blog.csdn.net/weixin_47936614/article/details/143932997) + +## Milvus Installation +本系统是在 Windows 11 上进行部署和运行的,关于milvus向量数据库的安装和启动可以参考以下步骤: + +1.勾选 `适用于Linux的Windows子系统` 和 `虚拟机平台` + +![09afdd4c8869124fd606715f57cae4cb](https://github.com/user-attachments/assets/67c0e85d-cdbc-4a9e-b724-df3f4ea1799a) + + +2.点击 `确定` 并重新启动计算机 + +3.以管理员身份打开命令提示符,输入以下命令安装WSL: +- 在 PowerShell 中设置 WSL 2 为默认版本: +``` +wsl --set-default-version 2 +``` +- 更新 WSL 内核,使用国内网络建议添加`--web-download`: +``` +wsl --update --web-download +``` +安装成功后的结果如下: + +![abe944eb639649fae89175ed09af9fdd](https://github.com/user-attachments/assets/74fc0c97-cf3c-47c6-b3a2-240ba97a44ff) + +4.下载安装docker-desktop +进入官网下载对应的版本安装即可,官网链接:[https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/) + +5.验证是否安装成功: +``` +docker --version +docker-compose --version +``` + +![a263610663c19d4046f28773a1126369](https://github.com/user-attachments/assets/1f8e801b-b2db-4644-83d7-5da10f4e5764) + +6.milvus向量数据库安装 +- 创建milvus文件夹,并在该文件夹下创建多个子文件夹,如下: + +![image](https://github.com/user-attachments/assets/27599318-2a9a-4358-a1ec-226ca8363921) + +- 下载milvus +进入下载页面:[https://github.com/milvus-io/milvus/releases](https://github.com/milvus-io/milvus/releases) +选择milvus版本及其对应的yml文件,点击下载即可,如下: + +![image](https://github.com/user-attachments/assets/5b8804ca-5c6b-4102-82ac-d47eee31c77b) + +- 将下载好的 `milvus-standalone-docker-compose.yml` 重命名为 `docker-compose.yml` ,并放入milvus文件中,如下: + +![bf7215948670e96f9b50f90a67463950](https://github.com/user-attachments/assets/177d4889-49ff-4af4-8515-7722dc65a504) + +- 在milvus文件夹中启动cmd命令,输入以下命令: +``` +docker compose up -d +docker compose ps +docker port milvus-standalone 19530/tcp +``` +运行结果如下: + +![694be819cd99abc328136139f2e8e8d2](https://github.com/user-attachments/assets/2506153f-9625-42dd-b09c-db01e5ded5d6) + +![1a3086ad4519524717991dffa47cce29](https://github.com/user-attachments/assets/a8e9a22c-2b1c-4c95-9e15-fe8d6cb0c3ee) + +至此,milvus数据库部署成功! + +7.Attu图形化界面安装 +下载地址:[https://github.com/zilliztech/attu/releases](https://github.com/zilliztech/attu/releases) +选择对应的版本直接下载安装即可: + +![image](https://github.com/user-attachments/assets/b4e96c46-e5e8-42e0-aae9-670008d3ff53) + +## Environment Installation +``` +conda create --name rag-env python=3.10 +cd Knowledge_QA_RAG +conda activate rag-env +pip install -r requirements.txt +``` +## Quick Start +1.启动milvus数据库 + +![image](https://github.com/user-attachments/assets/4d2a37ad-3f74-4b72-969c-685c6b023e96) + +2.启动系统服务 +``` +python main.py +``` +或 +``` +uvicorn server:app --reload --host 127.0.0.1 --port 8000 +``` + +3.访问页面: 在浏览器中输入下面url地址即可访问 +``` +http://127.0.0.1:8000/ +``` + +## 项目演示示例: +📺 [点击观看项目演示视频](https://www.bilibili.com/video/BV1a49KBqE7n/) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..e6df79f --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`AI-Meet/Knowledge_QA_RAG` +- 原始仓库:https://github.com/AI-Meet/Knowledge_QA_RAG +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/config.py b/config.py new file mode 100644 index 0000000..6188478 --- /dev/null +++ b/config.py @@ -0,0 +1,36 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/10/31 14:30 +# @blog: https://blog.csdn.net/weixin_47936614 + +import os + +import torch +from dotenv import load_dotenv + +# 从 .env 文件加载环境变量 +load_dotenv() + + +class RagConfig: + # FastAPI 服务配置 + fastapi_host = os.getenv("fastapi_host") + fastapi_port = os.getenv("fastapi_port") + + # Milvus 配置 + milvus_host = os.getenv("milvus_host") + milvus_port = os.getenv("milvus_port") + milvus_collection_name = os.getenv("milvus_collection_name") + + # OpenAI 设置 + base_url = os.getenv("openai_base_url") + api_key = os.getenv("openai_api_key") + llm_model_name = os.getenv("openai_llm_model_name") + + # 嵌入模型路径 + text_embeddings_model_path = os.getenv("text_embeddings_model_path") + + # 设备设置 + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + diff --git a/file_process.py b/file_process.py new file mode 100644 index 0000000..225151e --- /dev/null +++ b/file_process.py @@ -0,0 +1,45 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/10/31 13:59 +# @blog: https://blog.csdn.net/weixin_47936614 + +import os + +from langchain_community.document_loaders import TextLoader, UnstructuredWordDocumentLoader, PyPDFLoader + +from text_utils.text_split import RagTextSplitter + + +class RagFileProcessor(object): + def __init__(self, chunk_size: int = 512): + self.text_splitter = RagTextSplitter(chunk_size=chunk_size) + + def file_process(self, file_path: str): + if not os.path.exists(file_path): + raise FileNotFoundError(f"文件 {file_path} 不存在!") + + if file_path.lower().endswith(".txt"): + txt_loader = TextLoader(file_path, autodetect_encoding=True) + txt_docs = txt_loader.load_and_split(text_splitter=self.text_splitter) + return txt_docs + elif file_path.lower().endswith(".docx"): + docx_loader = UnstructuredWordDocumentLoader(file_path, mode="single") + docx_docs = docx_loader.load_and_split(text_splitter=self.text_splitter) + return docx_docs + elif file_path.lower().endswith(".pdf"): + pdf_loader = PyPDFLoader(file_path) + pdf_docs = pdf_loader.load_and_split(text_splitter=self.text_splitter) + return pdf_docs + else: + raise TypeError("文件类型不支持,目前仅支持:txt/docx/pdf") + + def get_data(self, file_path: str): + docs = self.file_process(file_path) + passage_docs = [doc.page_content.strip() for doc in docs] + file_name = {"source": os.path.basename(docs[0].metadata['source'])} + ids = [str(i) for i in range(len(passage_docs))] # 确保每个文档有唯一的 ID + meta_datas = [file_name for _ in range(len(passage_docs))] # 定义元数据信息,包括文件名等 + dict_data = {"texts": passage_docs, "ids": ids, "meta_datas": meta_datas} + return dict_data + diff --git a/main.py b/main.py new file mode 100644 index 0000000..97cd03b --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/11/26 10:14 +# @blog: https://blog.csdn.net/weixin_47936614 + +import uvicorn + +from milvus_vector import config + +if __name__ == '__main__': + # 主函数启动方式 + uvicorn.run( + "server:app", # 指定模块名和应用实例 + host=config.fastapi_host, # 本地地址 + port=int(config.fastapi_port), # 端口 + reload=True # 开启热重载 + ) diff --git a/milvus_vector.py b/milvus_vector.py new file mode 100644 index 0000000..c87d982 --- /dev/null +++ b/milvus_vector.py @@ -0,0 +1,37 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/11/22 18:53 +# @blog: https://blog.csdn.net/weixin_47936614 + +from langchain_milvus import Milvus + +from config import RagConfig +from text_utils.text_embeddings import RagTextEmbeddings + +# 加载配置 +config = RagConfig() + +# 配置索引参数和搜索参数 +index_params = { + "index_type": "IVF_FLAT", + "metric_type": "L2", + "params": {"nlist": 100} +} + +search_params = { + "metric_type": "L2", + "params": {"nprobe": 10} +} + +# 初始化 Milvus 向量存储 +vector_store = Milvus( + embedding_function=RagTextEmbeddings(embed_model_path=config.text_embeddings_model_path, + batch_size=32, + device=config.device), + collection_name=config.milvus_collection_name, + consistency_level="Bounded", + connection_args={"host": config.milvus_host, "port": config.milvus_port}, + index_params=index_params, + search_params=search_params +) diff --git a/protocol/__init__.py b/protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/protocol/mode.py b/protocol/mode.py new file mode 100644 index 0000000..8b745f8 --- /dev/null +++ b/protocol/mode.py @@ -0,0 +1,12 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/11/20 16:33 +# @blog: https://blog.csdn.net/weixin_47936614 + +from pydantic import BaseModel + + +class ChatRequest(BaseModel): + question: str + diff --git a/protocol/prompts.py b/protocol/prompts.py new file mode 100644 index 0000000..71b73d4 --- /dev/null +++ b/protocol/prompts.py @@ -0,0 +1,13 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/11/20 16:17 +# @blog: https://blog.csdn.net/weixin_47936614 + +prompt_template = """ +Use the following pieces of context to answer the question at the end. +If you don't know the answer, just say "sorry, I can't answer this question.", don't try to make up an answer. +{context} +Question: {question} +Answer in Chinese: +""" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8407f52 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,153 @@ +accelerate==1.1.1 +aiohappyeyeballs==2.4.3 +aiohttp==3.10.10 +aiosignal==1.3.1 +annotated-types==0.7.0 +anyio==4.6.2.post1 +async-timeout==4.0.3 +attrs==24.2.0 +backoff==2.2.1 +beautifulsoup4==4.12.3 +cbor==1.0.0 +certifi==2024.8.30 +cffi==1.17.1 +chardet==5.2.0 +charset-normalizer==3.4.0 +click==8.1.7 +colorama==0.4.6 +cryptography==43.0.3 +dataclasses-json==0.6.7 +datasets==2.19.0 +dill==0.3.8 +distro==1.9.0 +dnspython==2.7.0 +einops==0.8.0 +email_validator==2.2.0 +emoji==2.14.0 +environs==9.5.0 +eval_type_backport==0.2.0 +exceptiongroup==1.2.2 +fastapi==0.115.5 +fastapi-cli==0.0.5 +filelock==3.16.1 +filetype==1.2.0 +FlagEmbedding==1.3.2 +frozenlist==1.5.0 +fsspec==2024.3.1 +greenlet==3.1.1 +grpcio==1.67.1 +h11==0.14.0 +html5lib==1.1 +httpcore==1.0.6 +httptools==0.6.4 +httpx==0.27.2 +httpx-sse==0.4.0 +huggingface-hub==0.26.2 +idna==3.10 +ijson==3.3.0 +inscriptis==2.5.0 +ir_datasets==0.5.9 +itsdangerous==2.2.0 +Jinja2==3.1.4 +jiter==0.7.0 +joblib==1.4.2 +jsonpatch==1.33 +jsonpath-python==1.0.6 +jsonpointer==3.0.0 +langchain==0.3.7 +langchain-community==0.3.7 +langchain-core==0.3.18 +langchain-milvus==0.1.7 +langchain-openai==0.2.6 +langchain-text-splitters==0.3.0 +langdetect==1.0.9 +langsmith==0.1.137 +lxml==5.3.0 +lz4==4.3.3 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +marshmallow==3.23.0 +mdurl==0.1.2 +mpmath==1.3.0 +multidict==6.1.0 +multiprocess==0.70.16 +mypy-extensions==1.0.0 +nest-asyncio==1.6.0 +networkx==3.4.2 +nltk==3.9.1 +numpy==1.26.4 +olefile==0.47 +openai==1.54.3 +orjson==3.10.10 +packaging==24.1 +pandas==2.2.3 +peft==0.13.2 +pillow==11.0.0 +propcache==0.2.0 +protobuf==5.28.3 +psutil==6.1.0 +pyarrow==18.0.0 +pyarrow-hotfix==0.6 +pycparser==2.22 +pydantic==2.9.2 +pydantic-extra-types==2.10.0 +pydantic-settings==2.6.0 +pydantic_core==2.23.4 +Pygments==2.18.0 +pymilvus==2.4.8 +pypdf==5.1.0 +python-dateutil==2.8.2 +python-docx==1.1.2 +python-dotenv==1.0.1 +python-iso639==2024.10.22 +python-magic==0.4.27 +python-multipart==0.0.17 +python-oxmsg==0.0.1 +pytz==2024.2 +PyYAML==6.0.2 +RapidFuzz==3.10.1 +regex==2024.9.11 +requests==2.32.3 +requests-toolbelt==1.0.0 +rich==13.9.4 +safetensors==0.4.5 +scikit-learn==1.5.2 +scipy==1.14.1 +sentence-transformers==3.1.0 +sentencepiece==0.2.0 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +soupsieve==2.6 +SQLAlchemy==2.0.35 +starlette==0.41.3 +sympy==1.13.1 +tenacity==8.5.0 +threadpoolctl==3.5.0 +tiktoken==0.8.0 +tokenizers==0.19.1 +torch==2.5.1 +torchaudio==2.5.1 +torchvision==0.20.1 +tqdm==4.66.6 +transformers==4.44.2 +trec-car-tools==2.6 +typer==0.13.1 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +tzdata==2024.2 +ujson==5.10.0 +unlzw3==0.2.2 +unstructured==0.16.3 +unstructured-client==0.26.2 +urllib3==2.2.3 +uvicorn==0.32.0 +warc3-wet==0.2.5 +warc3-wet-clueweb09==0.2.5 +watchfiles==0.24.0 +webencodings==0.5.1 +websockets==14.1 +wrapt==1.16.0 +xxhash==3.5.0 +yarl==1.17.0 +zlib-state==0.1.9 diff --git a/server.py b/server.py new file mode 100644 index 0000000..25e24ce --- /dev/null +++ b/server.py @@ -0,0 +1,141 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/11/20 16:32 +# @blog: https://blog.csdn.net/weixin_47936614 + +import os +import shutil + +from fastapi import FastAPI, Request, File, UploadFile +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from langchain.chains.retrieval_qa.base import RetrievalQA +from langchain_core.prompts import PromptTemplate +from langchain_openai import ChatOpenAI +from pymilvus import Collection, connections +from pymilvus.orm import utility + +from milvus_vector import vector_store, config +from file_process import RagFileProcessor +from protocol.prompts import prompt_template +from protocol.mode import ChatRequest + +# 初始化 FastAPI 应用 +app = FastAPI(title="Knowledge_QA_RAG API", description="API for data process and retrieval using Milvus and LangChain.") + +# 挂载静态文件目录 +app.mount("/static", StaticFiles(directory="static"), name="static") + +# 配置模板目录 +templates = Jinja2Templates(directory="templates") + + +# 渲染主页 +@app.get("/", response_class=HTMLResponse) +async def read_home(request: Request): + return templates.TemplateResponse("qa.html", {"request": request}) + + +@app.post("/rag/chat/") +async def chat(request: ChatRequest): + """ + 根据用户问题,从向量库检索并返回回答。 + """ + print(f"Q: {request.question}") + try: + # 初始化 OpenAI Chat 模型 + llm = ChatOpenAI(model=config.llm_model_name, api_key=config.api_key, base_url=config.base_url) + # 定义 Prompt 模板 + qa_prompt = PromptTemplate(template=prompt_template, input_variables=["context", "question"]) + + # 定义搜索参数 + search_kwargs = {"score_threshold": 0.3, "k": 5} + retriever = vector_store.as_retriever(search_type="similarity_score_threshold", search_kwargs=search_kwargs) + qa_chain = RetrievalQA.from_chain_type( + llm=llm, + chain_type="stuff", + retriever=retriever, + chain_type_kwargs={"prompt": qa_prompt}, + return_source_documents=True + ) + result = qa_chain.invoke({"query": request.question}) + answer_result = result.get("result", "") + print(f"A: {answer_result}") + + source = {"source_documents": [{"content": doc.page_content, "metadata": doc.metadata} for doc in + result.get("source_documents", [])]} + print(f"source: {source}") + + return {"answer": answer_result} + except Exception as e: + return {"status": "error", "message": str(e)} + + +@app.post("/rag/clear/") +async def clear_knowledge(collection_name: str = config.milvus_collection_name, + host: str = config.milvus_host, + port: int = config.milvus_port): + """ + 清空 Milvus 知识库集合,并删除指定目录中的文件 + """ + folder = "./upload_files" + try: + connections.connect("default", host=host, port=port) + if utility.has_collection(collection_name): + collection = Collection(name=collection_name) + collection.drop() + print(f"Collection '{collection_name}' 成功删除.") + else: + print(f"Collection '{collection_name}' 不存在.") + connections.disconnect("default") + + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + + return {"message": f"知识库清空: Collection '{collection_name}' 删除, 文件夹 '{folder}' 清空."} + except Exception as e: + return {"error": f"知识库清空失败. 原因: {str(e)}"} + + +@app.post("/rag/create/") +async def create_knowledge(file: UploadFile = File(...)): + """ + 上传文件到指定目录后,处理文件内容并添加到向量库。 + """ + folder = './upload_files' # 文件存储目录 + os.makedirs(folder, exist_ok=True) # 确保目录存在 + file_path = os.path.join(folder, file.filename) + + try: + # 保存文件 + with open(file_path, "wb") as f: + content = await file.read() + f.write(content) + + folder: str = './upload_files' + file_path = os.path.join(folder, file.filename) + + # 初始化文件处理器 + file_processor = RagFileProcessor(chunk_size=64) + # 处理文件内容并插入到向量库 + text_datas = file_processor.get_data(file_path=file_path) + + # 连接到 Milvus + vector_store.add_texts(**text_datas) + return { + "status": "success", + "message": f"文件 '{file.filename}' 上传成功并添加至向量数据库.", + } + except Exception as e: + return { + "status": "error", + "message": f"文件 '{file.filename}'处理失败. 原因: {str(e)}", + } + + diff --git a/static/css/qa_style.css b/static/css/qa_style.css new file mode 100644 index 0000000..2a920ba --- /dev/null +++ b/static/css/qa_style.css @@ -0,0 +1,151 @@ +/*qa_css*/ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(135deg, #696971FF 0%, #788090FF 100%); /* 渐变背景 */ + background-size: cover; + color: #fff; +} + +#chat-container { + width: 50%; /* 宽度为页面的一半 */ + height: 80%; /* 高度为页面的 80% */ + display: flex; + flex-direction: column; + border-radius: 15px; /* 边角圆滑 */ + background-color: rgba(255, 255, 255, 0.9); /* 半透明背景 */ + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); /* 阴影效果 */ + overflow: hidden; +} + +#chat-box { + flex: 1; + padding: 20px; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: flex-start; + background: #f8f8f8; /* 聊天框背景 */ + border-radius: 10px; + margin: 10px; +} + +.message { + display: flex; + margin: 10px 0; + padding: 10px 15px; + border-radius: 10px; + max-width: 75%; + align-items: center; + font-size: 16px; +} + +.user-message { + align-self: flex-end; + background-color: #0084ff; + color: white; + flex-direction: row-reverse; +} + +.bot-message { + align-self: flex-start; + background-color: #e5e5e5; + color: black; + flex-direction: row; +} + +.avatar { + width: 45px; + height: 45px; + border-radius: 50%; + margin: 0 10px; +} + +#input-container { + display: flex; + padding: 15px; + border-top: 1px solid #ddd; + background-color: #fff; + justify-content: space-between; + align-items: center; +} + +#user-input { + flex: 1; + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 20px; + margin-right: 15px; + outline: none; + transition: all 0.3s ease; +} + +#user-input:focus { + border-color: #0084ff; /* 聚焦时边框颜色变化 */ + box-shadow: 0 0 5px rgba(0, 132, 255, 0.5); /* 聚焦时增加阴影效果 */ +} + +#send-button { + padding: 10px 20px; + font-size: 16px; + background-color: #0084ff; + color: white; + border: none; + border-radius: 30px; + cursor: pointer; + transition: all 0.3s ease; +} + +#send-button:hover { + background-color: #005bb5; /* 按钮悬停时的颜色 */ + transform: scale(1.05); /* 悬停时轻微放大 */ +} + +.message-container { + display: flex; + justify-content: flex-start; + align-items: center; +} + +.message-container-right { + justify-content: flex-end; +} + + +/*新添加的知识库按钮*/ +#kb-actions { + display: flex; + justify-content: center; + margin-top: 10px; + margin-bottom: 5px; +} + +#kb-actions button { + padding: 10px 20px; + font-size: 16px; + background-color: #56a750; /* 默认绿色 */ + color: white; + border: none; + border-radius: 30px; + cursor: pointer; + margin: 0 10px; + transition: all 0.3s ease; +} + +#kb-actions button:hover { + transform: scale(1.05); /* 悬停时轻微放大 */ +} + +#clear-knowledge { + background-color: #dc3545; /* 红色清空按钮 */ +} + +#clear-knowledge:hover { + background-color: #b22b37; /* 悬停时红色更深 */ +} diff --git a/static/images/bot.jpg b/static/images/bot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bb867a481293ca121b7afe2d4b1ec7450e2d8a02 GIT binary patch literal 4464 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 { + // 创建一个隐藏的文件输入框 + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".txt,.pdf,.docx"; // 根据需求限制文件类型 + fileInput.style.display = "none"; + + // 将文件输入框添加到页面 + document.body.appendChild(fileInput); + + // 监听文件选择事件 + fileInput.addEventListener("change", async () => { + const file = fileInput.files[0]; // 获取用户选择的文件 + if (!file) { + alert("请选择一个文件!"); + return; + } + + const formData = new FormData(); + formData.append("file", file); // 将文件添加到 FormData 对象 + + try { + // 发起 POST 请求到 /rag/add/ 接口 + const response = await fetch("/rag/create/", { + method: "POST", + body: formData, + }); + + const result = await response.json(); // 解析响应数据 + if (response.ok) { + alert(result.message || "文件上传并添加到知识库成功!"); + } else { + alert(result.message || "上传失败,请重试!"); + } + } catch (error) { + console.error("Error uploading file:", error); + alert("上传操作失败,请检查后端服务!"); + } finally { + // 从 DOM 中移除文件输入框 + document.body.removeChild(fileInput); + } + }); + + // 模拟点击文件输入框,触发文件选择对话框 + fileInput.click(); +}); + +// 监听清空知识库按钮 +const clearKnowledge = async () => { + try { + const response = await fetch("/rag/clear/", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + collection_name: "knowledge_collection", + host: "localhost", + port: 19530, + }), + }); + const result = await response.json(); + alert(result.message || result.error); + } catch (error) { + console.error("Error clearing knowledge:", error); + alert("Failed to clear knowledge."); + } +}; + +document.getElementById("clear-knowledge").addEventListener("click", clearKnowledge); + diff --git a/static/js/qa_script.js b/static/js/qa_script.js new file mode 100644 index 0000000..7cd122d --- /dev/null +++ b/static/js/qa_script.js @@ -0,0 +1,97 @@ +// qa_script.js +const chatBox = document.getElementById("chat-box"); +const userInput = document.getElementById("user-input"); +const sendButton = document.getElementById("send-button"); + +// 用户头像与机器人头像的路径,从静态资源目录加载 +const userAvatar = "./static/images/user.jpg"; +const botAvatar = "./static/images/bot.jpg"; + +async function sendMessage() { + const message = userInput.value.trim(); + if (!message) return; + + // 显示用户消息 + const userMessage = document.createElement("div"); + userMessage.textContent = message; + userMessage.className = "message user-message"; + + // 创建用户头像 + const userImage = document.createElement("img"); + userImage.src = userAvatar; + userImage.className = "avatar"; + + // 将用户头像和消息一起放到 message-container 中 + const userMessageContainer = document.createElement("div"); + userMessageContainer.className = "message-container message-container-right"; + userMessageContainer.appendChild(userImage); + userMessageContainer.appendChild(userMessage); + chatBox.appendChild(userMessageContainer); + chatBox.scrollTop = chatBox.scrollHeight; // 滚动到最底部 + + userInput.value = ""; // 清空输入框 + + // 发送问题到后端 + try { + const response = await fetch("/rag/chat/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ question: message }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // 显示机器人回复 + const botMessage = document.createElement("div"); + botMessage.textContent = data.answer || "No answer available."; + botMessage.className = "message bot-message"; + + // 创建机器人头像 + const botImage = document.createElement("img"); + botImage.src = botAvatar; + botImage.className = "avatar"; + + // 将机器人头像和消息一起放到 message-container 中 + const botMessageContainer = document.createElement("div"); + botMessageContainer.className = "message-container"; + botMessageContainer.appendChild(botImage); + botMessageContainer.appendChild(botMessage); + chatBox.appendChild(botMessageContainer); + chatBox.scrollTop = chatBox.scrollHeight; // 滚动到最底部 + } catch (error) { + console.error("Error:", error); + + const botMessage = document.createElement("div"); + botMessage.textContent = "An error occurred. Please try again."; + botMessage.className = "message bot-message"; + + // 创建机器人头像 + const botImage = document.createElement("img"); + botImage.src = botAvatar; + botImage.className = "avatar"; + + // 将头像和消息内容添加到消息容器 + const botMessageContainer = document.createElement("div"); + botMessageContainer.className = "message-container"; + botMessageContainer.appendChild(botImage); + botMessageContainer.appendChild(botMessage); + chatBox.appendChild(botMessageContainer); + chatBox.scrollTop = chatBox.scrollHeight; // 滚动到最底部 + } +} + +// 点击发送按钮时发送消息 +sendButton.addEventListener("click", sendMessage); + +// 按下 Enter 键发送消息 +userInput.addEventListener("keypress", (event) => { + if (event.key === "Enter") { + sendMessage(); + } +}); diff --git a/templates/qa.html b/templates/qa.html new file mode 100644 index 0000000..322dfd0 --- /dev/null +++ b/templates/qa.html @@ -0,0 +1,29 @@ + + + + + + 基于知识库的文档问答系统 + + + + + +
+
+
+ + +
+
+
+ + +
+
+
+ + + + + diff --git a/text_utils/__init__.py b/text_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/text_utils/text_embeddings.py b/text_utils/text_embeddings.py new file mode 100644 index 0000000..a1dc33a --- /dev/null +++ b/text_utils/text_embeddings.py @@ -0,0 +1,30 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/10/31 14:33 +# @blog: https://blog.csdn.net/weixin_47936614 + +from langchain_core.embeddings import Embeddings +from sentence_transformers import SentenceTransformer + + +class RagTextEmbeddings(Embeddings): + def __init__(self, embed_model_path: str, **kwargs): + self.batch_size = kwargs['batch_size'] + self.device = kwargs['device'] + self.embed_model = SentenceTransformer(embed_model_path, trust_remote_code=True, device=self.device) + + def embed_documents(self, texts: list[str]) -> list[list[float]]: + docs_embeddings = self.embed_model.encode(texts, + task="retrieval.passage", + batch_size=self.batch_size, + device=self.device, + show_progress_bar=True) + return docs_embeddings.tolist() + + def embed_query(self, text: str) -> list[float]: + query_embeddings = self.embed_model.encode([text], + task="retrieval.query", + device=self.device) + return query_embeddings.tolist()[0] + diff --git a/text_utils/text_split.py b/text_utils/text_split.py new file mode 100644 index 0000000..7e00bdf --- /dev/null +++ b/text_utils/text_split.py @@ -0,0 +1,76 @@ +# !/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author: CS_木成河 +# @time: 2024/10/31 11:17 +# @blog: https://blog.csdn.net/weixin_47936614 + +import re +from typing import List + +from langchain.text_splitter import CharacterTextSplitter + + +class RagTextSplitter(CharacterTextSplitter): + def __init__(self, chunk_size: int = 1024): + super().__init__() + self.chunk_size = chunk_size + + def split_text(self, text: str) -> List[str]: + text = re.sub(r"\n{3,}", "\n", text) # 移除三个或更多的连续换行符,用一个换行符代替 + text = re.sub(r'\s+', ' ', text) # 替换所有的空白字符为单个空格 + text = text.replace("\n\n", "") # 移除双换行符 + + sent_sep_pattern = re.compile(r'([﹒﹔﹖﹗.。!?]["’”」』]{0,2})') # 用于匹配中文句子结束标点符号以及紧随其后的引号 + sentences = [] + current_chunk = "" + + start = 0 + for match in sent_sep_pattern.finditer(text): + end = match.end() + sentence = text[start:end] + start = end + + # 检查当前块是否能容纳新句子 + if len(current_chunk) + len(sentence) > self.chunk_size: # 不能容纳 + if current_chunk: + sentences.append(current_chunk) + current_chunk = sentence + else: # 可以容纳 + current_chunk += sentence + + if len(sentences) == 0: + sentences.append(text.strip()) + + final_sentences = [] + for line in sentences: + if len(line) <= self.chunk_size: + final_sentences.append(line) + else: + final_sentences.extend(self.split_string(line, self.chunk_size)) + return final_sentences + + @staticmethod + def split_string(text: str, size: int) -> List[str]: + """ + Split the input string into chunks of specified size, splitting at the last space if needed. + + Parameters: + text (str): The input string to be split. + size (int): The size of each chunk. + + Returns: + list: A list containing the chunks of the input string. + """ + # 定义句子或标记分割符号列表 + SENTENCE_BREAK_SYMBOLS = [' ', '.', '!', '?', ',', ';', ':', '。', '?', '!', ',', ';', ':'] + chunks = [] + start = 0 + while start < len(text): + end = start + size + if end < len(text): + # 在最后一个空格处进行切分 + while end > start and text[end - 1] not in SENTENCE_BREAK_SYMBOLS: + end -= 1 + chunks.append(text[start:end]) + start = end + return chunks