From 3b55efbfab2663fb1c9808912714082ba6f68122 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Tue, 19 Aug 2025 07:34:39 +0800 Subject: [PATCH] 'commit' --- dsLightRag/Test/G2_TeachingStudent.py | 71 +++--- dsLightRag/Util/LlmUtil.py | 5 +- .../Util/__pycache__/LlmUtil.cpython-310.pyc | Bin 2066 -> 2185 bytes dsRag/.idea/RAG.iml | 2 +- dsRag/.idea/misc.xml | 2 +- dsSchoolBuddy/.idea/.gitignore | 8 + dsSchoolBuddy/.idea/RAG.iml | 12 + .../inspectionProfiles/profiles_settings.xml | 6 + dsSchoolBuddy/.idea/jsLibraryMappings.xml | 6 + dsSchoolBuddy/.idea/misc.xml | 7 + dsSchoolBuddy/.idea/modules.xml | 8 + dsSchoolBuddy/.idea/vcs.xml | 7 + dsSchoolBuddy/Config/Config.py | 27 ++ dsSchoolBuddy/Config/__init__.py | 0 .../Config/__pycache__/Config.cpython-310.pyc | Bin 0 -> 843 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 137 bytes dsSchoolBuddy/Doc/1、python环境配置.png | Bin 0 -> 77717 bytes dsSchoolBuddy/Doc/2、Conda维护.txt | 30 +++ dsSchoolBuddy/Doc/3、Pip维护.txt | 16 ++ .../Doc/4、Elasticsearch安装配置文档.md | 132 ++++++++++ .../ElasticSearch/T1_RebuildMapping.py | 47 ++++ dsSchoolBuddy/ElasticSearch/T2_SplitTxt.py | 191 ++++++++++++++ dsSchoolBuddy/ElasticSearch/T3_InsertData.py | 75 ++++++ .../ElasticSearch/T4_SelectAllData.py | 55 ++++ .../ElasticSearch/T5_SelectByTags.py | 79 ++++++ .../ElasticSearch/T6_XiangLiangQuery.py | 109 ++++++++ .../Utils/ElasticsearchCollectionManager.py | 111 ++++++++ .../Utils/ElasticsearchConnectionPool.py | 65 +++++ ...ticsearchCollectionManager.cpython-310.pyc | Bin 0 -> 3488 bytes ...lasticsearchConnectionPool.cpython-310.pyc | Bin 0 -> 2754 bytes dsSchoolBuddy/ElasticSearch/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 144 bytes dsSchoolBuddy/Start.py | 171 +++++++++++++ dsSchoolBuddy/Test/__init__.py | 0 dsSchoolBuddy/Util/EsSearchUtil.py | 238 ++++++++++++++++++ dsSchoolBuddy/Util/__init__.py | 0 .../__pycache__/ALiYunUtil.cpython-310.pyc | Bin 0 -> 1283 bytes .../__pycache__/EsSearchUtil.cpython-310.pyc | Bin 0 -> 6300 bytes .../__pycache__/SearchUtil.cpython-310.pyc | Bin 0 -> 3066 bytes .../Util/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 135 bytes 40 files changed, 1439 insertions(+), 41 deletions(-) create mode 100644 dsSchoolBuddy/.idea/.gitignore create mode 100644 dsSchoolBuddy/.idea/RAG.iml create mode 100644 dsSchoolBuddy/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 dsSchoolBuddy/.idea/jsLibraryMappings.xml create mode 100644 dsSchoolBuddy/.idea/misc.xml create mode 100644 dsSchoolBuddy/.idea/modules.xml create mode 100644 dsSchoolBuddy/.idea/vcs.xml create mode 100644 dsSchoolBuddy/Config/Config.py create mode 100644 dsSchoolBuddy/Config/__init__.py create mode 100644 dsSchoolBuddy/Config/__pycache__/Config.cpython-310.pyc create mode 100644 dsSchoolBuddy/Config/__pycache__/__init__.cpython-310.pyc create mode 100644 dsSchoolBuddy/Doc/1、python环境配置.png create mode 100644 dsSchoolBuddy/Doc/2、Conda维护.txt create mode 100644 dsSchoolBuddy/Doc/3、Pip维护.txt create mode 100644 dsSchoolBuddy/Doc/4、Elasticsearch安装配置文档.md create mode 100644 dsSchoolBuddy/ElasticSearch/T1_RebuildMapping.py create mode 100644 dsSchoolBuddy/ElasticSearch/T2_SplitTxt.py create mode 100644 dsSchoolBuddy/ElasticSearch/T3_InsertData.py create mode 100644 dsSchoolBuddy/ElasticSearch/T4_SelectAllData.py create mode 100644 dsSchoolBuddy/ElasticSearch/T5_SelectByTags.py create mode 100644 dsSchoolBuddy/ElasticSearch/T6_XiangLiangQuery.py create mode 100644 dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchCollectionManager.py create mode 100644 dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchConnectionPool.py create mode 100644 dsSchoolBuddy/ElasticSearch/Utils/__pycache__/ElasticsearchCollectionManager.cpython-310.pyc create mode 100644 dsSchoolBuddy/ElasticSearch/Utils/__pycache__/ElasticsearchConnectionPool.cpython-310.pyc create mode 100644 dsSchoolBuddy/ElasticSearch/__init__.py create mode 100644 dsSchoolBuddy/ElasticSearch/__pycache__/__init__.cpython-310.pyc create mode 100644 dsSchoolBuddy/Start.py create mode 100644 dsSchoolBuddy/Test/__init__.py create mode 100644 dsSchoolBuddy/Util/EsSearchUtil.py create mode 100644 dsSchoolBuddy/Util/__init__.py create mode 100644 dsSchoolBuddy/Util/__pycache__/ALiYunUtil.cpython-310.pyc create mode 100644 dsSchoolBuddy/Util/__pycache__/EsSearchUtil.cpython-310.pyc create mode 100644 dsSchoolBuddy/Util/__pycache__/SearchUtil.cpython-310.pyc create mode 100644 dsSchoolBuddy/Util/__pycache__/__init__.cpython-310.pyc diff --git a/dsLightRag/Test/G2_TeachingStudent.py b/dsLightRag/Test/G2_TeachingStudent.py index c819af6c..8470248c 100644 --- a/dsLightRag/Test/G2_TeachingStudent.py +++ b/dsLightRag/Test/G2_TeachingStudent.py @@ -3,45 +3,39 @@ import sys from Util import LlmUtil -def initialize_chat_history(): - """初始化对话历史,包含系统提示""" - system_prompt = """ - STRICT RULES -Be an approachable-yet-dynamic teacher,who helps the user learn by guiding -them through their studies. - -1.Get to know the user.lf you don't know their goals or grade level,ask the -user before diving in.(Keep this lightweight!)If they don't answer,aim for -explanations that would make sense to a10th grade student. - -2.Build on existing knowledge.Connect new ideas to what the user already -knows. - -3.Guide users,don't just give answers.Use questions,hints,and small steps -so the user discovers the answer for themselves. - -4.Check and reinforce.After hard parts,confirm the user can restate or use the -idea.Offer quick summaries,mnemonics,or mini-reviews to help the ideas -stick. - -5.Vary the rhythm.Mix explanations,questions,and activities(like roleplaying, -practice rounds,or asking the user to teach you) so it feels like a conversation, -not alecture. - -Above all:DO NOT DO THE USER'S WORK FOR THEM. Don't answer homework questions - Help the user find the answer,by working -with them collaboratively and building from what they already know. +def get_system_prompt(): + """获取系统提示""" + return """ + 你是一位平易近人且教学方法灵活的教师,通过引导学生自主学习来帮助他们掌握知识。 + + 严格遵循以下教学规则: + 1. 首先了解学生情况:在开始讲解前,询问学生的年级水平和对勾股定理的了解程度。 + 2. 基于现有知识构建:将新思想与学生已有的知识联系起来。 + 3. 引导而非灌输:使用问题、提示和小步骤,让学生自己发现答案。 + 4. 检查和强化:在讲解难点后,确认学生能够重述或应用这些概念。 + 5. 变化节奏:混合讲解、提问和互动活动,让教学像对话而非讲座。 + + 最重要的是:不要直接给出答案,而是通过合作和基于学生已有知识的引导,帮助学生自己找到答案。 """ - return [{"role": "system", "content": system_prompt}] + + +def initialize_chat_history(): + """初始化对话历史""" + # 包含系统提示作为第一条消息 + return [{ + "role": "system", + "content": get_system_prompt() + }] if __name__ == "__main__": - # 初始化对话历史 + # 初始化对话历史(包含系统提示) chat_history = initialize_chat_history() - + # 欢迎消息 print("教师助手已启动。输入 'exit' 或 '退出' 结束对话。") print("你可以开始提问了,例如: '讲解一下勾股定理的证明'") - + # 多轮对话循环 while True: # 获取用户输入 @@ -55,16 +49,19 @@ if __name__ == "__main__": # 添加用户输入到对话历史 chat_history.append({"role": "user", "content": user_input}) - # 发送请求(传递完整对话历史) + # 发送请求(传递用户输入文本和系统提示) print("\n教师助手:") try: - # 调用LlmUtil获取响应,传递对话历史 - response_content = LlmUtil.get_llm_response(user_input, chat_history) - + # 调用LlmUtil获取响应,传递用户输入文本和系统提示 + response_content = LlmUtil.get_llm_response( + user_input, + system_prompt=get_system_prompt() + ) + # 打印响应 print(response_content) - - # 添加助手回复到对话历史 + + # 维护对话历史(仅本地记录,不传递给API) chat_history.append({"role": "assistant", "content": response_content}) except Exception as e: print(f"发生错误: {str(e)}") diff --git a/dsLightRag/Util/LlmUtil.py b/dsLightRag/Util/LlmUtil.py index 12db6a5c..e00c2a87 100644 --- a/dsLightRag/Util/LlmUtil.py +++ b/dsLightRag/Util/LlmUtil.py @@ -54,11 +54,12 @@ async def get_llm_response_async(query_text: str, stream: bool = True): yield f"处理请求时发生异常: {str(e)}" # 保留原同步函数 -def get_llm_response(query_text: str, stream: bool = True): +def get_llm_response(query_text: str, stream: bool = True, system_prompt: str = 'You are a helpful assistant.'): """ 获取大模型的响应 @param query_text: 查询文本 @param stream: 是否使用流式输出 + @param system_prompt: 系统提示文本,默认为'You are a helpful assistant.' @return: 完整响应文本 """ client = OpenAI( @@ -70,7 +71,7 @@ def get_llm_response(query_text: str, stream: bool = True): completion = client.chat.completions.create( model=LLM_MODEL_NAME, messages=[ - {'role': 'system', 'content': 'You are a helpful assistant.'}, + {'role': 'system', 'content': system_prompt}, {'role': 'user', 'content': query_text} ], stream=stream diff --git a/dsLightRag/Util/__pycache__/LlmUtil.cpython-310.pyc b/dsLightRag/Util/__pycache__/LlmUtil.cpython-310.pyc index 1ddb25a0dc663bd2c926f58e61ee626bd0c0d0b0..bd138d4506b9c5ef1a37bbaf378c08c7564a35d3 100644 GIT binary patch delta 712 zcmY+C&ubGw6vtPe_xJSpa&ff80{lV(kJ*V#>=u)#|~ z1dBfw)I-5WdJ#mB9+bv^!G9npK}qaM{{-i4TS*_xXXbt1@Mhk;c{TlXrsNw&7D@Z^ z; zyw+8+Rb~gx+Cyixx=qvq#<^sc)Q%NoR*?golNe&>0}}Je8`7U!05Joi5w3=j)2;== zZwJu>RcZ&OCpWcIkJVUpRhPJ0M7a_xT-{JOi4n{VCB|G^S9_#^dRmOdjhI|V%NKhz zrY;jpKu%90SD#$FnZsU>#oA^zW_^rNG!-+>pzw7SqelfVdp-@4{X{u81KT*;G-9of zW&PRCZbE7Ic3=y;Z5}G-syW^sj0f*V&tHtUc1EwCj5a?0`Sxsg@b>qotzTbviue4E zX$!}+%@wB=)H^NH4#Q>`*MVI&|#7!p6dT36;_(z0bWLckguHyIi?hu0^ivFtneK>B&QZu|Je7 znuW<8`lxkXt8W*y^ewG?*%jK>xeMz^>BX9(^H{UBdB8mP`%1Dgs`_1#j z?6E||9Nx#~J9b=6tk?<^G>KZRv%p@AgWUgFB4csq#B$0x8|Pz5&(hA7$=PW+bY?yC zc}^k7(JpP@xv`jXd}4lb!_o~oN&m`fopgje0d-`sL|7(p!V5$TR*_y2jC5a>x+SWY z2^9j#k;%?*=!Y6MsWQ_>1W`Zq6wpTi2?1{i9wKwQlfj@3{%9D5BOl&UW2S1Iq8YZ+ zUuuJ^^tY-V?qb#BEiaeJ2yf~1frQT7W)yb&y~ccjS9Deos?-%9qG$m0EkNhzQy1s= J?27Sn=Ls@ncuoKS diff --git a/dsRag/.idea/RAG.iml b/dsRag/.idea/RAG.iml index f1d90d5d..2ef181ef 100644 --- a/dsRag/.idea/RAG.iml +++ b/dsRag/.idea/RAG.iml @@ -4,7 +4,7 @@ - + diff --git a/dsRag/.idea/misc.xml b/dsRag/.idea/misc.xml index 913c162f..4ccb922a 100644 --- a/dsRag/.idea/misc.xml +++ b/dsRag/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/dsSchoolBuddy/.idea/.gitignore b/dsSchoolBuddy/.idea/.gitignore new file mode 100644 index 00000000..a7cdac76 --- /dev/null +++ b/dsSchoolBuddy/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/dsSchoolBuddy/.idea/RAG.iml b/dsSchoolBuddy/.idea/RAG.iml new file mode 100644 index 00000000..2ef181ef --- /dev/null +++ b/dsSchoolBuddy/.idea/RAG.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/dsSchoolBuddy/.idea/inspectionProfiles/profiles_settings.xml b/dsSchoolBuddy/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/dsSchoolBuddy/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/dsSchoolBuddy/.idea/jsLibraryMappings.xml b/dsSchoolBuddy/.idea/jsLibraryMappings.xml new file mode 100644 index 00000000..1bde7079 --- /dev/null +++ b/dsSchoolBuddy/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dsSchoolBuddy/.idea/misc.xml b/dsSchoolBuddy/.idea/misc.xml new file mode 100644 index 00000000..4ccb922a --- /dev/null +++ b/dsSchoolBuddy/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/dsSchoolBuddy/.idea/modules.xml b/dsSchoolBuddy/.idea/modules.xml new file mode 100644 index 00000000..23b163c9 --- /dev/null +++ b/dsSchoolBuddy/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/dsSchoolBuddy/.idea/vcs.xml b/dsSchoolBuddy/.idea/vcs.xml new file mode 100644 index 00000000..61aaccdb --- /dev/null +++ b/dsSchoolBuddy/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/dsSchoolBuddy/Config/Config.py b/dsSchoolBuddy/Config/Config.py new file mode 100644 index 00000000..94344ebc --- /dev/null +++ b/dsSchoolBuddy/Config/Config.py @@ -0,0 +1,27 @@ +# Elasticsearch配置 +ES_CONFIG = { + "hosts": "https://127.0.0.1:9200", + "basic_auth": ("elastic", "jv9h8uwRrRxmDi1dq6u8"), + "verify_certs": False, + "ssl_show_warn": False, + "index_name": "ds_kb" +} + +# 嵌入向量模型 +EMBED_MODEL_NAME = "BAAI/bge-m3" +EMBED_API_KEY = "sk-pbqibyjwhrgmnlsmdygplahextfaclgnedetybccknxojlyl" +EMBED_BASE_URL = "https://api.siliconflow.cn/v1" +EMBED_DIM = 1024 +EMBED_MAX_TOKEN_SIZE = 8192 + +# 重排模型 +RERANK_MODEL = 'BAAI/bge-reranker-v2-m3' +RERANK_BASE_URL = 'https://api.siliconflow.cn/v1/rerank' +RERANK_BINDING_API_KEY = 'sk-pbqibyjwhrgmnlsmdygplahextfaclgnedetybccknxojlyl' + +# 阿里云API信息【HZKJ】 +ALY_LLM_API_KEY = "sk-01d13a39e09844038322108ecdbd1bbc" +ALY_LLM_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" +ALY_LLM_MODEL_NAME = "qwen-plus" +#ALY_LLM_MODEL_NAME = "deepseek-r1" +# ALY_LLM_MODEL_NAME = "deepseek-v3" \ No newline at end of file diff --git a/dsSchoolBuddy/Config/__init__.py b/dsSchoolBuddy/Config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dsSchoolBuddy/Config/__pycache__/Config.cpython-310.pyc b/dsSchoolBuddy/Config/__pycache__/Config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82eb2f5e7887007c2cbe63d681767ca4e295b3d2 GIT binary patch literal 843 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 YkB{(LG2d!no)E+=i+Sc(r#lDzKfc_m#sB~S literal 0 HcmV?d00001 diff --git a/dsSchoolBuddy/Doc/2、Conda维护.txt b/dsSchoolBuddy/Doc/2、Conda维护.txt new file mode 100644 index 00000000..021764bf --- /dev/null +++ b/dsSchoolBuddy/Doc/2、Conda维护.txt @@ -0,0 +1,30 @@ +# 添加Anaconda的TUNA镜像 +conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ + +# 设置搜索时显示通道地址 +conda config --set show_channel_urls yes + +# 创建虚拟环境 +conda create -n rag python=3.10 + +# 查看当前存在哪些虚拟环境 +conda env list +conda info -e + +# 查看安装了哪些包 +conda list + +# 激活虚拟环境 +conda activate rag + +# 对虚拟环境中安装额外的包 +conda install -n rag $package_name + +# 删除虚拟环境 +conda remove -n rag --all + +# 删除环境中的某个包 +conda remove --name rag $package_name + +# 恢复默认镜像 +conda config --remove-key channels diff --git a/dsSchoolBuddy/Doc/3、Pip维护.txt b/dsSchoolBuddy/Doc/3、Pip维护.txt new file mode 100644 index 00000000..081e047b --- /dev/null +++ b/dsSchoolBuddy/Doc/3、Pip维护.txt @@ -0,0 +1,16 @@ +# 激活虚拟环境 +conda activate rag + +# 永久修改pip源为阿里云镜像源(适用于Windows系统) +pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ + +# 验证是否修改成功 +pip config list + +global.index-url='https://mirrors.aliyun.com/pypi/simple/' + +# 获取依赖了哪些包 +pip freeze > requirements.txt + +# 新机器安装包 +pip install -r D:\dsWork\dsProject\dsRag\requirements.txt \ No newline at end of file diff --git a/dsSchoolBuddy/Doc/4、Elasticsearch安装配置文档.md b/dsSchoolBuddy/Doc/4、Elasticsearch安装配置文档.md new file mode 100644 index 00000000..cac6d013 --- /dev/null +++ b/dsSchoolBuddy/Doc/4、Elasticsearch安装配置文档.md @@ -0,0 +1,132 @@ +### 一、安装 $ES$ + +**1、下载安装包** + +进入官网下载$linux$安装包 [下载地址](https://www.elastic.co/cn/downloads/elasticsearch) + +![img](https://i-blog.csdnimg.cn/direct/04ae4c7f65fe475fb19e913eaf80ba04.png) + +**2、安装$JDK$$21$** + +```sh +sudo yum install java-21-openjdk-devel +echo 'export JAVA_HOME=/usr/lib/jvm/java-21-openjdk +export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc + +source ~/.bashrc +``` + +**3、上传文件到$linux$服务器** + +```sh +# 如果没有 rz 命令 先安装 +yum -y install lrzsz + +# rz 打开弹窗 选择下载好的文件 确认 在哪个目录下执行,就会上传到该目录下 +rz -be +``` + + **4、新建用户并设置密码** + +```sh +# 创建用户 +useradd elauser + +# 设置密码 符合密码规范 大写 + 小写 + 数字 + 特殊字符 + 大于八位 +passwd elauser + +#输入密码: +DsideaL@123 + +tar -zxvf elasticsearch-9.0.2-linux-x86_64.tar.gz +sudo chown -R elauser:elauser /usr/local/elasticsearch-9.0.2 +# 进入解压文件并编辑配置文件 +cd elasticsearch-9.0.2/config +vi elasticsearch.yml +# 修改数据目录和日志目录 +mkdir -p /usr/local/elasticsearch-9.0.2/data +mkdir -p /usr/local/elasticsearch-9.0.2/logs +``` + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/20250623130022571.png) + +设置允许所有IP进行访问,在添加下面参数让$elasticsearch-head$插件可以访问$es$ + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/20250623130217136.png) + +```yaml +network.host: 0.0.0.0 +http.cors.enabled: true +http.cors.allow-origin: "*" +``` + +**5、修改系统配置** + +```sh +# m.max_map_count 值太低 +# 临时解决方案(需要root权限) +sudo sysctl -w vm.max_map_count=262144 + +# 永久解决方案(需要root权限) +echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf +sudo sysctl -p + +# 验证是否有效 +sysctl vm.max_map_count +``` + +**6、启动** + +```sh +# 启动 +su - elauser + +cd /usr/local/elasticsearch-9.0.2/bin +# ./elasticsearch-keystore create + +# 启动 -d = damon 守护进程 +./elasticsearch -d + + +# 访问地址 +https://10.10.14.206:9200 + +# 日志文件 +/usr/local/elasticsearch-9.0.2/logs/elasticsearch.log +``` + + 弹出输入账号密码,这里需要重置下密码,再登录 进入安装目录的bin目录下 + +执行下面命令 就会在控制台打印出新密码 账号就是 elastic + +``` +./elasticsearch-reset-password -u elastic +``` + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/20250623132315148.png) + +登录成功,完活。 + +```sh +elastic +jv9h8uwRrRxmDi1dq6u8 +``` + + + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/20250623132417828.png) + +> **注意**:如果访问不到,请检查是否开启了$VPN$ + +### 二、安装$ik$中文分词插件 + +```bash +# 安装分词插件 +./bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/9.0.2 + +# 检查插件列表 +[elauser@maxkb elasticsearch-9.0.2]$ ./bin/elasticsearch-plugin list +analysis-ik +``` + +![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/20250623133924355.png) diff --git a/dsSchoolBuddy/ElasticSearch/T1_RebuildMapping.py b/dsSchoolBuddy/ElasticSearch/T1_RebuildMapping.py new file mode 100644 index 00000000..0d5799bf --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/T1_RebuildMapping.py @@ -0,0 +1,47 @@ +import warnings + +from elasticsearch import Elasticsearch + +from Config import Config + +# 抑制HTTPS相关警告 +warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') +warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + +# 初始化ES连接 +es = Elasticsearch( + hosts=Config.ES_CONFIG['hosts'], + basic_auth=Config.ES_CONFIG['basic_auth'], + verify_certs=False +) + +# 定义mapping结构 +mapping = { + "mappings": { + "properties": { + "embedding": { + "type": "dense_vector", + "dims": 200, # embedding维度为200 + "index": True, + "similarity": "l2_norm" # 使用L2距离 + }, + "user_input": {"type": "text"}, + "tags": { + "type": "object", + "properties": { + "tags": {"type": "keyword"}, + "full_content": {"type": "text"} + } + } + } + } +} + +# 创建索引 +index_name = Config.ES_CONFIG['index_name'] +if es.indices.exists(index=index_name): + es.indices.delete(index=index_name) + print(f"删除已存在的索引 '{index_name}'") + +es.indices.create(index=index_name, body=mapping) +print(f"索引 '{index_name}' 创建成功,mapping结构已设置。") \ No newline at end of file diff --git a/dsSchoolBuddy/ElasticSearch/T2_SplitTxt.py b/dsSchoolBuddy/ElasticSearch/T2_SplitTxt.py new file mode 100644 index 00000000..7cfa4a61 --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/T2_SplitTxt.py @@ -0,0 +1,191 @@ +import os +import re +import shutil +import warnings +import zipfile + +from docx import Document +from docx.oxml.ns import nsmap + +from Util import DocxUtil + +# 抑制HTTPS相关警告 +warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') +warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + + +def extract_images_from_docx(docx_path, output_folder): + """ + 从docx提取图片并记录位置 + :param docx_path: Word文档路径 + :param output_folder: 图片输出文件夹 + :return: 包含图片路径和位置的列表 + """ + # 从docx_path 的名称示例:小学数学教学中的若干问题_MATH_1.docx + # 则图片的前缀统一为 MATH_1_?.docx ,其中 ? 为数字,表示图片的序号 + # 先获取到前缀 + a = docx_path.split("_") + prefix = a[1] + "_" + a[2].split(".")[0] + # print(f"图片前缀为:{prefix}") + # 创建一个List 记录每个图片的名称和序号 + image_data = [] + # 创建临时解压目录 + temp_dir = os.path.join(output_folder, "temp_docx") + os.makedirs(temp_dir, exist_ok=True) + + # 解压docx文件 + with zipfile.ZipFile(docx_path, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # 读取主文档关系 + with open(os.path.join(temp_dir, 'word', '_rels', 'document.xml.rels'), 'r') as rels_file: + rels_content = rels_file.read() + + # 加载主文档 + doc = Document(docx_path) + img_counter = 1 + + # 遍历所有段落 + for para_idx, paragraph in enumerate(doc.paragraphs): + for run_idx, run in enumerate(paragraph.runs): + # 检查运行中的图形 + for element in run._element: + if element.tag.endswith('drawing'): + # 提取图片关系ID + blip = element.find('.//a:blip', namespaces=nsmap) + if blip is not None: + embed_id = blip.get('{%s}embed' % nsmap['r']) + + # 从关系文件中获取图片文件名 + rel_entry = f' 2 else line + elif line and line[0].isdigit(): + line = line[1:] + line = line.strip() + if in_block and line: # 只添加非空行 + current_block.append(line) + + if current_block: + blocks.append('\n'.join(current_block)) + + return [(i + 1, block) for i, block in enumerate(blocks)] + + +def save_to_txt(content, file_path, mode='w'): + """将内容保存到文本文件""" + try: + with open(file_path, mode, encoding='utf-8') as f: + f.write(content) + return True + except Exception as e: + print(f"保存文件{file_path}时出错: {str(e)}") + return False + + +class ImageReplacer: + def __init__(self, image_list): + self.image_list = image_list + self.current_idx = 0 + + def replace(self, match): + if self.current_idx < len(self.image_list): + result = f"![](./Images/{self.image_list[self.current_idx]})" + self.current_idx += 1 + return result + return match.group() + + +def process_document(docx_file, txt_output_dir, img_output_dir): + # 提取图片 + listImage = extract_images_from_docx(docx_file, img_output_dir) + print(f"图片数量为:{len(listImage)}") + + # 读取内容 + res = DocxUtil.get_docx_content_by_pandoc(docx_file) + # 分块 + chunks = split_into_blocks(res) + saved_count = 0 + + # 使用原来的正则表达式 + pattern = re.compile(r'【图片\d+】') + # 创建图片替换器 + replacer = ImageReplacer(listImage) + + for x in chunks: + firstLine = x[1].split("\n")[0].strip() + content = x[1][len(firstLine):].strip() + + # 使用类方法替换图片 + content = pattern.sub(replacer.replace, content) + # 保存文本文件 + # 从docx文件名提取学科和编号 + docx_name = os.path.basename(docx_file).split('.')[0] + subject_part = '_'.join(docx_name.split('_')[-2:]) # 获取最后两部分如CHINESE_1 + output_file = os.path.join(txt_output_dir, f"{subject_part}_{x[0]}.txt") + full_content = f"{firstLine}\n{content}" + if save_to_txt(full_content, output_file, mode='w'): + saved_count += 1 + + print(f"处理完成,共保存{saved_count}个文件到目录: {txt_output_dir}") + + +if __name__ == "__main__": + txt_output_dir = "../Txt/" + img_output_dir = "../static/Images/" + # 清空上面的两个输出目录,用os进行删除,在Windows环境中进行 + if os.path.exists(txt_output_dir): + shutil.rmtree(txt_output_dir) + if os.path.exists(img_output_dir): + shutil.rmtree(img_output_dir) + # 创建输出目录 + os.makedirs(txt_output_dir, exist_ok=True) + os.makedirs(img_output_dir, exist_ok=True) + + # 遍历static/Txt/下所有的docx + for filename in os.listdir("../static/Txt/"): + print("正在处理文件:" + filename) + # 这里需要文件的全称路径 + filename = os.path.join("../static/Txt/", filename) + process_document(filename, txt_output_dir, img_output_dir) diff --git a/dsSchoolBuddy/ElasticSearch/T3_InsertData.py b/dsSchoolBuddy/ElasticSearch/T3_InsertData.py new file mode 100644 index 00000000..a2d8921b --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/T3_InsertData.py @@ -0,0 +1,75 @@ +import warnings + +from Config import Config +from Config.Config import * +from elasticsearch import Elasticsearch +from gensim.models import KeyedVectors +import jieba +import os +import time + +# 抑制HTTPS相关警告 +warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') +warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + +# 1. 加载预训练的 Word2Vec 模型 +model_path = MODEL_PATH +model = KeyedVectors.load_word2vec_format(model_path, binary=False, limit=MODEL_LIMIT) +print(f"模型加载成功,词向量维度: {model.vector_size}") + + +# 功能:将文本转换为嵌入向量 +def text_to_embedding(text): + words = jieba.lcut(text) + embeddings = [model[word] for word in words if word in model] + if embeddings: + return sum(embeddings) / len(embeddings) + return [0.0] * model.vector_size + + +# 2. 初始化Elasticsearch连接 +es = Elasticsearch( + hosts=Config.ES_CONFIG['hosts'], + basic_auth=Config.ES_CONFIG['basic_auth'], + verify_certs=False +) + +# 3. 处理processed_chunks目录下的所有文件 +txt_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'Txt') + +for filename in os.listdir(txt_dir): + if filename.endswith('.txt'): + filepath = os.path.join(txt_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + # 只读取第一行作为向量计算 + first_line = f.readline().strip() + # 读取全部内容用于后续查询 + full_content = first_line + '\n' + f.read() + + if not first_line: + print(f"跳过空文件: {filename}") + continue + + print(f"正在处理文件: {filename}") + + # 4. 获取当前时间和会话ID + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + # 需要进行标记的标签 + x = filename.split("_") + selectedTags = [x[0] + "_" + x[1]] + tags = {"tags": selectedTags, "full_content": full_content} # 添加完整内容 + + # 5. 将第一行文本转换为嵌入向量 + embedding = text_to_embedding(first_line) + + # 6. 插入数据到Elasticsearch + doc = { + 'tags': tags, + 'user_input': first_line, + 'timestamp': timestamp, + 'embedding': embedding + } + es.index(index=ES_CONFIG['index_name'], document=doc) + print(f"文件 {filename} 数据插入成功") + +print("所有文件处理完成") diff --git a/dsSchoolBuddy/ElasticSearch/T4_SelectAllData.py b/dsSchoolBuddy/ElasticSearch/T4_SelectAllData.py new file mode 100644 index 00000000..99418857 --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/T4_SelectAllData.py @@ -0,0 +1,55 @@ +import warnings + +from elasticsearch import Elasticsearch + +from Config import Config + +# 抑制HTTPS相关警告 +warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') +warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + + +# 初始化Elasticsearch连接 +es = Elasticsearch( + hosts=Config.ES_CONFIG['hosts'], + basic_auth=Config.ES_CONFIG['basic_auth'], + verify_certs=False +) + +# 查询所有数据 +def select_all_data(index_name): + try: + # 构建查询条件 - 匹配所有文档 + # 修改查询条件为获取前10条数据 + query = { + "query": { + "match_all": {} + }, + "size": 1000 # 仅获取10条数据 + } + + # 执行查询 + response = es.search(index=index_name, body=query) + hits = response['hits']['hits'] + + if not hits: + print(f"索引 {index_name} 中没有数据") + else: + print(f"索引 {index_name} 中共有 {len(hits)} 条数据:") + for i, hit in enumerate(hits, 1): + print(f"{i}. ID: {hit['_id']}") + print(f" 内容: {hit['_source'].get('user_input', '')}") + print(f" 标签: {hit['_source'].get('tags', '')}") + print(f" 时间戳: {hit['_source'].get('timestamp', '')}") + # 在循环中添加打印完整数据结构的代码 + if i == 1: # 只打印第一条数据的完整结构 + print("完整数据结构:") + import pprint + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(hit['_source']) + print("-" * 50) + except Exception as e: + print(f"查询出错: {e}") + +if __name__ == "__main__": + select_all_data(Config.ES_CONFIG['index_name']) \ No newline at end of file diff --git a/dsSchoolBuddy/ElasticSearch/T5_SelectByTags.py b/dsSchoolBuddy/ElasticSearch/T5_SelectByTags.py new file mode 100644 index 00000000..ae5e5d62 --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/T5_SelectByTags.py @@ -0,0 +1,79 @@ +from elasticsearch import Elasticsearch +import warnings +from Config import Config +from Config.Config import ES_CONFIG +import urllib3 + +# 抑制HTTPS相关警告 +warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') +warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + + +# 1. 初始化Elasticsearch连接 +es = Elasticsearch( + hosts=Config.ES_CONFIG['hosts'], + basic_auth=Config.ES_CONFIG['basic_auth'], + verify_certs=False +) + +# 2. 直接在代码中指定要查询的标签 +query_tag = ["MATH_1"] # 可以修改为其他需要的标签 + +# 3. 构建查询条件 +query = { + "query": { + "bool": { + "should": [ + { + "terms": { + "tags.tags": query_tag + } + } + ], + "minimum_should_match": 1 + + } + }, + "size": 1000 +} + +# 4. 执行查询并处理结果 +try: + response = es.search(index="knowledge_base", body=query) + hits = response['hits']['hits'] + + if not hits: + print(f"未找到标签为 '{query_tag}' 的数据。") + else: + print(f"找到 {len(hits)} 条标签为 '{query_tag}' 的数据:") + for i, hit in enumerate(hits, 1): + print(f"{i}. ID: {hit['_id']}") + print(f" 内容: {hit['_source'].get('user_input', '')}") + print(f" 标签: {hit['_source']['tags']['tags']}") + print("-" * 50) +except Exception as e: + print(f"查询执行失败,错误详情: {str(e)}") + print(f"查询条件: {query}") + if hasattr(e, 'info') and isinstance(e.info, dict): + print(f"Elasticsearch错误详情: {e.info}") + if hasattr(e, 'status_code'): + print(f"HTTP状态码: {e.status_code}") + print(f"查询出错: {str(e)}") + +# 4. 执行查询 +try: + results = es.search(index=ES_CONFIG['index_name'], body=query) + print(f"查询标签 '{query_tag}' 结果:") + if results['hits']['hits']: + for hit in results['hits']['hits']: + doc = hit['_source'] + print(f"ID: {hit['_id']}") + print(f"标签: {doc['tags']['tags']}") + print(f"用户问题: {doc['user_input']}") + print(f"时间: {doc['timestamp']}") + print(f"向量: {doc['embedding'][:5]}...") + print("-" * 40) + else: + print(f"未找到标签为 '{query_tag}' 的数据。") +except Exception as e: + print(f"查询失败: {e}") \ No newline at end of file diff --git a/dsSchoolBuddy/ElasticSearch/T6_XiangLiangQuery.py b/dsSchoolBuddy/ElasticSearch/T6_XiangLiangQuery.py new file mode 100644 index 00000000..215ab7ad --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/T6_XiangLiangQuery.py @@ -0,0 +1,109 @@ +import logging +import warnings + +from Config.Config import ES_CONFIG +from Util.EsSearchUtil import EsSearchUtil + +# 初始化日志 +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# 初始化EsSearchUtil +esClient = EsSearchUtil(ES_CONFIG) +# 抑制HTTPS相关警告 +warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') +warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + +if __name__ == "__main__": + # 测试查询 + # query = "小学数学中有哪些模型" + query = "文言虚词" + query_tags = ["MATH_1"] # 默认搜索标签,可修改 + print(f"\n=== 开始执行查询 ===") + print(f"原始查询文本: {query}") + + # 执行混合搜索 + es_conn = esClient.es_pool.get_connection() + try: + # 向量搜索 + print("\n=== 向量搜索阶段 ===") + print("1. 文本分词和向量化处理中...") + query_embedding = esClient.text_to_embedding(query) + print(f"2. 生成的查询向量维度: {len(query_embedding)}") + print(f"3. 前3维向量值: {query_embedding[:3]}") + + print("4. 正在执行Elasticsearch向量搜索...") + vector_results = es_conn.search( + index=ES_CONFIG['index_name'], + body={ + "query": { + "script_score": { + "query": { + "bool": { + "should": [ + { + "terms": { + "tags.tags": query_tags + } + } + ], + "minimum_should_match": 1 + } + }, + "script": { + "source": "double score = cosineSimilarity(params.query_vector, 'embedding'); return score >= 0 ? score : 0", + "params": {"query_vector": query_embedding} + } + } + }, + "size": 3 + } + ) + print(f"5. 向量搜索结果数量: {len(vector_results['hits']['hits'])}") + + # 文本精确搜索 + print("\n=== 文本精确搜索阶段 ===") + print("1. 正在执行Elasticsearch文本精确搜索...") + text_results = es_conn.search( + index=ES_CONFIG['index_name'], + body={ + "query": { + "bool": { + "must": [ + { + "match": { + "user_input": query + } + }, + { + "terms": { + "tags.tags": query_tags + } + } + ] + } + }, + "size": 3 + } + ) + print(f"2. 文本搜索结果数量: {len(text_results['hits']['hits'])}") + + # 打印详细结果 + print("\n=== 最终搜索结果 ===") + + vector_int = 0 + for i, hit in enumerate(vector_results['hits']['hits'], 1): + if hit['_score'] > 0.4: # 阀值0.4 + print(f" {i}. 文档ID: {hit['_id']}, 相似度分数: {hit['_score']:.2f}") + print(f" 内容: {hit['_source']['user_input']}") + vector_int = vector_int + 1 + print(f" 向量搜索结果: {vector_int}条") + + print("\n文本精确搜索结果:") + for i, hit in enumerate(text_results['hits']['hits']): + print(f" {i + 1}. 文档ID: {hit['_id']}, 匹配分数: {hit['_score']:.2f}") + print(f" 内容: {hit['_source']['user_input']}") + # print(f" 详细: {hit['_source']['tags']['full_content']}") + + finally: + esClient.es_pool.release_connection(es_conn) diff --git a/dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchCollectionManager.py b/dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchCollectionManager.py new file mode 100644 index 00000000..4db6a138 --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchCollectionManager.py @@ -0,0 +1,111 @@ +from elasticsearch import Elasticsearch +from elasticsearch.exceptions import NotFoundError +import logging + +logger = logging.getLogger(__name__) + + +class ElasticsearchCollectionManager: + def __init__(self, index_name): + """ + 初始化Elasticsearch索引管理器 + :param index_name: Elasticsearch索引名称 + """ + self.index_name = index_name + + def load_collection(self, es_connection): + """ + 加载索引,如果不存在则创建 + :param es_connection: Elasticsearch连接 + """ + try: + if not es_connection.indices.exists(index=self.index_name): + logger.warning(f"Index {self.index_name} does not exist, creating new index") + self._create_index(es_connection) + except Exception as e: + logger.error(f"Failed to load collection: {str(e)}") + raise + + def _create_index(self, es_connection): + """创建新的Elasticsearch索引""" + mapping = { + "mappings": { + "properties": { + "user_input": {"type": "text"}, + "tags": { + "type": "object", + "properties": { + "tags": {"type": "keyword"}, + "full_content": {"type": "text"} + } + }, + "timestamp": {"type": "date"}, + "embedding": {"type": "dense_vector", "dims": 200} + } + } + } + es_connection.indices.create(index=self.index_name, body=mapping) + + def search(self, es_connection, query_embedding, search_params, expr=None, limit=5): + """ + 执行混合搜索(向量+关键字) + :param es_connection: Elasticsearch连接 + :param query_embedding: 查询向量 + :param search_params: 搜索参数 + :param expr: 过滤表达式 + :param limit: 返回结果数量 + :return: 搜索结果 + """ + try: + # 构建查询 + query = { + "query": { + "script_score": { + "query": { + "bool": { + "must": [] + } + }, + "script": { + "source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0", + "params": {"query_vector": query_embedding} + } + } + }, + "size": limit + } + + # 添加标签过滤条件 + if expr: + query["query"]["script_score"]["query"]["bool"]["must"].append( + {"nested": { + "path": "tags", + "query": { + "terms": {"tags.tags": expr.split(" OR ")} + } + }} + ) + + logger.info(f"Executing search with query: {query}") + response = es_connection.search(index=self.index_name, body=query) + return response["hits"]["hits"] + except Exception as e: + logger.error(f"Search failed: {str(e)}") + raise + + def query_by_id(self, es_connection, doc_id): + """ + 根据ID查询文档 + :param es_connection: Elasticsearch连接 + :param doc_id: 文档ID + :return: 文档内容 + """ + try: + response = es_connection.get(index=self.index_name, id=doc_id) + return response["_source"] + except NotFoundError: + logger.warning(f"Document with id {doc_id} not found") + return None + except Exception as e: + logger.error(f"Failed to query document by id: {str(e)}") + raise \ No newline at end of file diff --git a/dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchConnectionPool.py b/dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchConnectionPool.py new file mode 100644 index 00000000..88a877fc --- /dev/null +++ b/dsSchoolBuddy/ElasticSearch/Utils/ElasticsearchConnectionPool.py @@ -0,0 +1,65 @@ +from elasticsearch import Elasticsearch +import threading +import logging + +logger = logging.getLogger(__name__) + + +class ElasticsearchConnectionPool: + def __init__(self, hosts, basic_auth, verify_certs=False, max_connections=50): + """ + 初始化Elasticsearch连接池 + :param hosts: Elasticsearch服务器地址 + :param basic_auth: 认证信息(username, password) + :param verify_certs: 是否验证SSL证书 + :param max_connections: 最大连接数 + """ + self.hosts = hosts + self.basic_auth = basic_auth + self.verify_certs = verify_certs + self.max_connections = max_connections + self._connections = [] + self._lock = threading.Lock() + self._initialize_pool() + + def _initialize_pool(self): + """初始化连接池""" + for _ in range(self.max_connections): + self._connections.append(self._create_connection()) + + def _create_connection(self): + """创建新的Elasticsearch连接""" + return Elasticsearch( + hosts=self.hosts, + basic_auth=self.basic_auth, + verify_certs=self.verify_certs + ) + + def get_connection(self): + """从连接池获取一个连接""" + with self._lock: + if not self._connections: + logger.warning("Connection pool exhausted, creating new connection") + return self._create_connection() + return self._connections.pop() + + def release_connection(self, connection): + """释放连接回连接池""" + with self._lock: + if len(self._connections) < self.max_connections: + self._connections.append(connection) + else: + try: + connection.close() + except Exception as e: + logger.warning(f"Failed to close connection: {str(e)}") + + def close(self): + """关闭所有连接""" + with self._lock: + for conn in self._connections: + try: + conn.close() + except Exception as e: + logger.warning(f"Failed to close connection: {str(e)}") + self._connections.clear() \ No newline at end of file diff --git a/dsSchoolBuddy/ElasticSearch/Utils/__pycache__/ElasticsearchCollectionManager.cpython-310.pyc b/dsSchoolBuddy/ElasticSearch/Utils/__pycache__/ElasticsearchCollectionManager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..059711511b9f6f86065ee1d816861205a065ccb9 GIT binary patch literal 3488 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 KikW}}3j+WHkssOs literal 0 HcmV?d00001 diff --git a/dsSchoolBuddy/Start.py b/dsSchoolBuddy/Start.py new file mode 100644 index 00000000..239a94c3 --- /dev/null +++ b/dsSchoolBuddy/Start.py @@ -0,0 +1,171 @@ +import json +import subprocess +import tempfile +import urllib.parse +import uuid +import warnings +from io import BytesIO + +import fastapi +import uvicorn +from fastapi import FastAPI, HTTPException +from openai import AsyncOpenAI +from sse_starlette import EventSourceResponse +from starlette.responses import StreamingResponse +from starlette.staticfiles import StaticFiles + +from Config import Config +from Util.EsSearchUtil import * +from Util.MySQLUtil import init_mysql_pool + +# 初始化日志 +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# 配置日志处理器 +log_file = os.path.join(os.path.dirname(__file__), 'Logs', 'app.log') +os.makedirs(os.path.dirname(log_file), exist_ok=True) + +# 文件处理器 +file_handler = RotatingFileHandler( + log_file, maxBytes=1024 * 1024, backupCount=5, encoding='utf-8') +file_handler.setFormatter(logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + +# 控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setFormatter(logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) + +logger.addHandler(file_handler) +logger.addHandler(console_handler) + +# 初始化异步 OpenAI 客户端 +client = AsyncOpenAI( + api_key=Config.MODEL_API_KEY, + base_url=Config.MODEL_API_URL, +) + + +async def lifespan(app: FastAPI): + # 抑制HTTPS相关警告 + warnings.filterwarnings('ignore', message='Connecting to .* using TLS with verify_certs=False is insecure') + warnings.filterwarnings('ignore', message='Unverified HTTPS request is being made to host') + yield + + +app = FastAPI(lifespan=lifespan) + +# 挂载静态文件目录 +app.mount("/static", StaticFiles(directory="Static"), name="static") + + +@app.post("/api/save-word") +async def save_to_word(request: fastapi.Request): + output_file = None + try: + # Parse request data + try: + data = await request.json() + markdown_content = data.get('markdown_content', '') + if not markdown_content: + raise ValueError("Empty MarkDown content") + except Exception as e: + logger.error(f"Request parsing failed: {str(e)}") + raise HTTPException(status_code=400, detail=f"Invalid request: {str(e)}") + + # 创建临时Markdown文件 + temp_md = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex + ".md") + with open(temp_md, "w", encoding="utf-8") as f: + f.write(markdown_content) + + # 使用pandoc转换 + output_file = os.path.join(tempfile.gettempdir(), "【理想大模型】问答.docx") + subprocess.run(['pandoc', temp_md, '-o', output_file, '--resource-path=static'], check=True) + + # 读取生成的Word文件 + with open(output_file, "rb") as f: + stream = BytesIO(f.read()) + + # 返回响应 + encoded_filename = urllib.parse.quote("【理想大模型】问答.docx") + return StreamingResponse( + stream, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"}) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + # 清理临时文件 + try: + if temp_md and os.path.exists(temp_md): + os.remove(temp_md) + if output_file and os.path.exists(output_file): + os.remove(output_file) + except Exception as e: + logger.warning(f"Failed to clean up temp files: {str(e)}") + + +@app.post("/api/rag", response_model=None) +async def rag(request: fastapi.Request): + data = await request.json() + query = data.get('query', '') + query_tags = data.get('tags', []) + # 调用es进行混合搜索 + search_results = EsSearchUtil.queryByEs(query, query_tags, logger) + # 构建提示词 + context = "\n".join([ + f"结果{i + 1}: {res['tags']['full_content']}" + for i, res in enumerate(search_results['text_results']) + ]) + # 添加图片识别提示 + prompt = f""" + 信息检索与回答助手 + 根据以下关于'{query}'的相关信息: + + 相关信息 + {context} + + 回答要求 + 1. 对于公式内容: + - 使用行内格式:$公式$ + - 重要公式可单独一行显示 + - 绝对不要使用代码块格式(```或''') + - 可适当使用\large增大公式字号 + - 如果内容中包含数学公式,请使用行内格式,如$f(x) = x^2$ + - 如果内容中包含多个公式,请使用行内格式,如$f(x) = x^2$ $g(x) = x^3$ + 2. 如果没有提供任何资料,那就直接拒绝回答,明确不在知识范围内。 + 3. 如果发现提供的资料与要询问的问题都不相关,就拒绝回答,明确不在知识范围内。 + 4. 如果发现提供的资料中只有部分与问题相符,那就只提取有用的相关部分,其它部分请忽略。 + 5. 对于符合问题的材料中,提供了图片的,尽量保持上下文中的图片,并尽量保持图片的清晰度。 + """ + + async def generate_response_stream(): + try: + # 流式调用大模型 + stream = await client.chat.completions.create( + model=Config.MODEL_NAME, + messages=[ + {'role': 'user', 'content': prompt} + ], + max_tokens=8000, + stream=True # 启用流式模式 + ) + # 流式返回模型生成的回复 + async for chunk in stream: + if chunk.choices[0].delta.content: + yield f"data: {json.dumps({'reply': chunk.choices[0].delta.content}, ensure_ascii=False)}\n\n" + + except Exception as e: + yield f"data: {json.dumps({'error': str(e)})}\n\n" + + return EventSourceResponse(generate_response_stream()) + + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/dsSchoolBuddy/Test/__init__.py b/dsSchoolBuddy/Test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dsSchoolBuddy/Util/EsSearchUtil.py b/dsSchoolBuddy/Util/EsSearchUtil.py new file mode 100644 index 00000000..a9a2e7ec --- /dev/null +++ b/dsSchoolBuddy/Util/EsSearchUtil.py @@ -0,0 +1,238 @@ +import logging +import os +from logging.handlers import RotatingFileHandler +import jieba +from gensim.models import KeyedVectors + +from Config.Config import MODEL_LIMIT, MODEL_PATH, ES_CONFIG +from ElasticSearch.Utils.ElasticsearchConnectionPool import ElasticsearchConnectionPool + +# 初始化日志 +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +# 确保日志目录存在 +os.makedirs('Logs', exist_ok=True) +handler = RotatingFileHandler('Logs/start.log', maxBytes=1024 * 1024, backupCount=5) +handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) +logger.addHandler(handler) + +class EsSearchUtil: + def __init__(self, es_config): + """ + 初始化Elasticsearch搜索工具 + :param es_config: Elasticsearch配置字典,包含hosts, username, password, index_name等 + """ + self.es_config = es_config + + # 初始化连接池 + self.es_pool = ElasticsearchConnectionPool( + hosts=es_config['hosts'], + basic_auth=es_config['basic_auth'], + verify_certs=es_config.get('verify_certs', False), + max_connections=50 + ) + + # 保留直接连接用于兼容 + from elasticsearch import Elasticsearch + self.es = Elasticsearch( + hosts=es_config['hosts'], + basic_auth=es_config['basic_auth'], + verify_certs=es_config.get('verify_certs', False) + ) + + # 确保es_conn属性存在以兼容旧代码 + self.es_conn = self.es + + # 确保es_conn属性存在以兼容旧代码 + self.es_conn = self.es + + + # 加载预训练模型 + self.model = KeyedVectors.load_word2vec_format(MODEL_PATH, binary=False, limit=MODEL_LIMIT) + logger.info(f"模型加载成功,词向量维度: {self.model.vector_size}") + + # 初始化Elasticsearch连接 + self.es = Elasticsearch( + hosts=es_config['hosts'], + basic_auth=es_config['basic_auth'], + verify_certs=False + ) + self.index_name = es_config['index_name'] + + def text_to_embedding(self, text): + # 使用已加载的模型 + # 对文本分词并计算平均向量 + words = jieba.lcut(text) + vectors = [self.model[word] for word in words if word in self.model] + + if not vectors: + return [0.0] * self.model.vector_size + + # 计算平均向量 + avg_vector = [sum(dim)/len(vectors) for dim in zip(*vectors)] + return avg_vector + + def vector_search(self, query, size=10): + query_embedding = self.text_to_embedding(query) + script_query = { + "script_score": { + "query": {"match_all": {}}, + "script": { + "source": "double score = cosineSimilarity(params.query_vector, 'embedding'); return score >= 0 ? score : 0", + "params": {"query_vector": query_embedding} + } + } + } + + return self.es_conn.search( + index=self.es_config['index_name'], + query=script_query, + size=size + ) + + def text_search(self, query, size=10): + return self.es_conn.search( + index=self.es_config['index_name'], + query={"match": {"user_input": query}}, + size=size + ) + + def hybrid_search(self, query, size=10): + """ + 执行混合搜索(向量搜索+文本搜索) + :param query: 搜索查询文本 + :param size: 返回结果数量 + :return: 包含两种搜索结果的字典 + """ + vector_results = self.vector_search(query, size) + text_results = self.text_search(query, size) + + return { + 'vector_results': vector_results, + 'text_results': text_results + } + + def search(self, query, search_type='hybrid', size=10): + """ + 统一搜索接口 + :param query: 搜索查询文本 + :param search_type: 搜索类型('vector', 'text' 或 'hybrid') + :param size: 返回结果数量 + :return: 搜索结果 + """ + if search_type == 'vector': + return self.vector_search(query, size) + elif search_type == 'text': + return self.text_search(query, size) + else: + return self.hybrid_search(query, size) + + def queryByEs(query, query_tags, logger): + # 获取EsSearchUtil实例 + es_search_util = EsSearchUtil(ES_CONFIG) + + # 执行混合搜索 + es_conn = es_search_util.es_pool.get_connection() + try: + # 向量搜索 + logger.info(f"\n=== 开始执行查询 ===") + logger.info(f"原始查询文本: {query}") + logger.info(f"查询标签: {query_tags}") + + logger.info("\n=== 向量搜索阶段 ===") + logger.info("1. 文本分词和向量化处理中...") + query_embedding = es_search_util.text_to_embedding(query) + logger.info(f"2. 生成的查询向量维度: {len(query_embedding)}") + logger.info(f"3. 前3维向量值: {query_embedding[:3]}") + + logger.info("4. 正在执行Elasticsearch向量搜索...") + vector_results = es_conn.search( + index=ES_CONFIG['index_name'], + body={ + "query": { + "script_score": { + "query": { + "bool": { + "should": [ + { + "terms": { + "tags.tags": query_tags + } + } + ], + "minimum_should_match": 1 + } + }, + "script": { + "source": "double score = cosineSimilarity(params.query_vector, 'embedding'); return score >= 0 ? score : 0", + "params": {"query_vector": query_embedding} + } + } + }, + "size": 3 + } + ) + # 处理一下,判断是否到达阀值 + filtered_vector_hits = [] + vector_int = 0 + for hit in vector_results['hits']['hits']: + if hit['_score'] > 0.8: # 阀值0.8 + # 新增语义相关性检查 + if all(word in hit['_source']['user_input'] for word in jieba.lcut(query)): + logger.info(f" {vector_int + 1}. 文档ID: {hit['_id']}, 相似度分数: {hit['_score']:.2f}") + logger.info(f" 内容: {hit['_source']['user_input']}") + filtered_vector_hits.append(hit) + vector_int += 1 + + # 更新vector_results只包含通过过滤的文档 + vector_results['hits']['hits'] = filtered_vector_hits + logger.info(f"5. 向量搜索结果数量(过滤后): {vector_int}") + + # 文本精确搜索 + logger.info("\n=== 文本精确搜索阶段 ===") + logger.info("1. 正在执行Elasticsearch文本精确搜索...") + text_results = es_conn.search( + index=ES_CONFIG['index_name'], + body={ + "query": { + "bool": { + "must": [ + { + "match": { + "user_input": query + } + }, + { + "terms": { + "tags.tags": query_tags + } + } + ] + } + }, + "size": 3 + } + ) + logger.info(f"2. 文本搜索结果数量: {len(text_results['hits']['hits'])}") + + # 合并vector和text结果 + all_sources = [hit['_source'] for hit in vector_results['hits']['hits']] + \ + [hit['_source'] for hit in text_results['hits']['hits']] + + # 去重处理 + unique_sources = [] + seen_user_inputs = set() + + for source in all_sources: + if source['user_input'] not in seen_user_inputs: + seen_user_inputs.add(source['user_input']) + unique_sources.append(source) + + logger.info(f"合并后去重结果数量: {len(unique_sources)}条") + + search_results = { + "text_results": unique_sources + } + return search_results + finally: + es_search_util.es_pool.release_connection(es_conn) \ No newline at end of file diff --git a/dsSchoolBuddy/Util/__init__.py b/dsSchoolBuddy/Util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dsSchoolBuddy/Util/__pycache__/ALiYunUtil.cpython-310.pyc b/dsSchoolBuddy/Util/__pycache__/ALiYunUtil.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b26f07edce13f41717347954bcdd6f2c8ef60600 GIT binary patch literal 1283 zcmZuw&2Jk;6rY(LuQ%H`jf#XQ2jr_Ia8!;KLPTwZDq6RqLQz&htL@GtUU$8_of%VN zIjB;DT0}}!VTsVvqM{rawJadQ`IYnmN&ckun=!GZ{NImA2aX$UR)~K z2q=$;{-bN4lG#(#t_3HYGDUsEt8P64993uhhp?DVg{pYh|z1rDX_u? zR53^3gDN;R&m}yXJzXJb>Fl{vb7$Py^QYbS<`z;r-_~c(&iUGQoDMR-23n3B%#cGE zW@HUH1~VCDg*EI@WU-y~I~i z>!K&Q+ZJJ^AP8g<`UR=!hXIe2(3ePU?H0O`*WyqXDtWl{BN9j8=%3e5)fSnYkHsbU zofmPF`wFD@z2!yysKvwgRTIeNx?p%ssM9xO88hQ1Sv9&wv%u(a)HIvLd8img%3667qgCjB zr^E_sqD!ygyXYR7MI8xAkfJq^{RBE-jl-@FzLL;+1 z1#EEZ_HOUnQSa+vzh5iYE4Hven^M2wDd&jBT*y@rM-s3kxTm<7(C0l*4 zII7hXb07OW(e|k!l>jX2MyF<8Krsy)$36_2VxHkIXOnO$jseqxd>I%i+StMtAur75 z)8-U@6w zOg$;m1ZE)%PZfpM)hniB!aPP^kj++F%KKH#f7z&IBbHhJU+C0&E5Fg(y6{>2Ni>a* Gk$(ZA_ims7 literal 0 HcmV?d00001 diff --git a/dsSchoolBuddy/Util/__pycache__/EsSearchUtil.cpython-310.pyc b/dsSchoolBuddy/Util/__pycache__/EsSearchUtil.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7e38fcb68d4db9b8cc03898999fd483917e12fb GIT binary patch literal 6300 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 literal 0 HcmV?d00001 diff --git a/dsSchoolBuddy/Util/__pycache__/SearchUtil.cpython-310.pyc b/dsSchoolBuddy/Util/__pycache__/SearchUtil.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab6219cd1431100b177818bf374db5fdbf4df1d9 GIT binary patch literal 3066 zcma)8|8pC~72n-Eou#uR%ZcOI!3oE=B!VFF1^R=D-A+jg{1Q6#w9MQ%qbk~qd_lUC z_wLeIbDA-=6FaR<4J{1Q*o_S|G&4_kFE@O=AHZsO@ag#OEk&W8Z;3wWK!VW5cOA}V1WW4zy!LD(IqhU z0Y=vYFdlAUX!m+TA6b$3t{ZK8NPA|;33U3gaZ-&|W5W8Qe)I~WkNGkBI6o!&1g!Q$ zJO{4GD@x1yLExr=ySIIklm8L=Pc|_+JOqR%IuM@nqqJu?x7|iL^nauLK%b_WS=5f< z1e|kKrOya%3srEt3#b}*V>9S7^1H!{o@!zqt*U;4`z>OLuXg(hzFNYQ&x3T*S3|i_ zW*(i#OPHPZlO1xez{#pJ7{=rb9zfL|H$I2XVtSY!nT>M&8D85daT-Vc9*FappMvq( zm9S2eew;qHOsc(p@2g1MfP^n|AI7#(e?aMPyRwPv6^o z_w)MA&xEZ5pmHRmg(hnAmo`3Hti82R6T-EHcWNKZHkK~k{^CX^lkqf=J(|%Pf4f?r zUuyh)HiQag8@E2KeRT~0cY)Bcj8?n6c#QKxL(}WS+9vM=-cv9<3wSr)uPy&8jO=7F z=eTCx5q%_72;BtCKs7CDT*z9vlA*_)JTuE~*2&w<2;?)iT?`_w!Aedg=H?1ch7X$< z5Yj|foC(`2QoXNaT4u>BWkWh!%DMT8%I~S|ohcfcFr|%XdD}59W6Ug>#T+x;se|Pl z%axqWq-U_H?0F;a+U$^az$l$DC^fCZf%NklGhB~ZO%tOd+F|YIO=4I(tj9Z5(juvQ*)b~y-xY!LDCv~97j~Y5L+!|6m(qV!+``DW4nPPh93`|7#^q2t2TQLre8Dr9Ml}Fev>PV^WWC-_RnmlJQb+Ni>Bk| z?NWJkoP%-?`cP%BD5+mgopdtCi*`O&bVgyZCHX*~8Nalwf6Es(&{_%qcn@JiFr4NK zUW-3z=@@J{2Vr=fN8hs*J@iw=h>OCrn8Ck87)=t2;hTam{*`bFz9XcfVFAf=!D296 z$9l>&F1XotojKuQac6?q@q%F)7s_n345Y&=gG?(Zj@7#gmhV=Ee(D^yR6PD!a(n$< zXm9m5E^DHGHK?-9xy9NCiyJpTu6_OXck2rqfBIwNt84JDRk}3Se$B8PxR%MV7i5{? zctzI<;=;=&l}<8Vv5aR!?=qZh8CsN{EEsM!Z(9~*)U>TYHm$R^-YXPcJKO3hY#*pW zI}m*^h#8hwGFZ+v0?9GlK*~`XD7kXkuqbf6lD>l(MI+}J9k$au!X~1I&4>5HKq=aV zg2D78_!HXBdQecpKkk*@6AHKe;9Z10%ALU`+rwkoiaNY9jvhw6MeXz~6KrHfL`|m1 zqQ0xS(>%21UXWl$$v$sT@Z7Up=<7L7gl8Nin@%>*G|c+L+@&z}t<*+<=Ujs!N>oY4 zDoNoKj4D=fA0B{}j1^2ILI~`VyW+1){sHC$6v-X=dsy|!-zs;8_NbDIo6?woo{&|V z^N)f`$=`uwzbfs+d+>gERWg8|gt=ep$Fij0sFcJ>SVdt3Ytt#<4@iKIl{>OLMA$Rn z>Zx=@PrP7TXU#$;oWf%1<##;O`7s?<64z-riH;i(Jzlb@S2RX>A955J;!yr^>3<+- Bw-*2a literal 0 HcmV?d00001 diff --git a/dsSchoolBuddy/Util/__pycache__/__init__.cpython-310.pyc b/dsSchoolBuddy/Util/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79767003c9ac32bf4142003b854ef98e2eacfc07 GIT binary patch literal 135 zcmd1j<>g`kf?)2*3=sVoL?8o3AjbiSi&=m~3PUi1CZpd