aifeifei798 commited on
Commit
8178a13
·
verified ·
1 Parent(s): 9daa3b6

Upload 2 files

Browse files
Files changed (2) hide show
  1. requirements.txt +129 -0
  2. web_app.py +294 -0
requirements.txt ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.13.2
3
+ aiosignal==1.4.0
4
+ altair==6.0.0
5
+ annotated-types==0.7.0
6
+ anyio==4.12.0
7
+ attrs==25.4.0
8
+ beautifulsoup4==4.14.3
9
+ blinker==1.9.0
10
+ cachetools==6.2.4
11
+ certifi==2025.11.12
12
+ charset-normalizer==3.4.4
13
+ click==8.3.1
14
+ dataclasses-json==0.6.7
15
+ distro==1.9.0
16
+ faiss-cpu==1.13.2
17
+ filelock==3.20.2
18
+ frozenlist==1.8.0
19
+ fsspec==2025.12.0
20
+ gitdb==4.0.12
21
+ gitpython==3.1.46
22
+ greenlet==3.3.0
23
+ h11==0.16.0
24
+ hf-xet==1.2.1
25
+ httpcore==1.0.9
26
+ httpx==0.28.1
27
+ httpx-sse==0.4.3
28
+ huggingface-hub==0.36.0
29
+ idna==3.11
30
+ jinja2==3.1.6
31
+ jiter==0.12.0
32
+ joblib==1.5.3
33
+ jsonpatch==1.33
34
+ jsonpointer==3.0.0
35
+ jsonschema==4.25.1
36
+ jsonschema-specifications==2025.9.1
37
+ langchain==1.2.0
38
+ langchain-classic==1.0.1
39
+ langchain-community==0.4.1
40
+ langchain-core==1.2.6
41
+ langchain-huggingface==1.2.0
42
+ langchain-openai==1.1.6
43
+ langchain-tavily==0.2.16
44
+ langchain-text-splitters==1.1.0
45
+ langgraph==1.0.5
46
+ langgraph-checkpoint==3.0.1
47
+ langgraph-prebuilt==1.0.5
48
+ langgraph-sdk==0.3.1
49
+ langsmith==0.6.0
50
+ markupsafe==3.0.3
51
+ marshmallow==3.26.2
52
+ mpmath==1.3.0
53
+ multidict==6.7.0
54
+ mypy-extensions==1.1.0
55
+ narwhals==2.14.0
56
+ networkx==3.6.1
57
+ numpy==2.4.0
58
+ nvidia-cublas-cu12==12.8.4.1
59
+ nvidia-cuda-cupti-cu12==12.8.90
60
+ nvidia-cuda-nvrtc-cu12==12.8.93
61
+ nvidia-cuda-runtime-cu12==12.8.90
62
+ nvidia-cudnn-cu12==9.10.2.21
63
+ nvidia-cufft-cu12==11.3.3.83
64
+ nvidia-cufile-cu12==1.13.1.3
65
+ nvidia-curand-cu12==10.3.9.90
66
+ nvidia-cusolver-cu12==11.7.3.90
67
+ nvidia-cusparse-cu12==12.5.8.93
68
+ nvidia-cusparselt-cu12==0.7.1
69
+ nvidia-nccl-cu12==2.27.5
70
+ nvidia-nvjitlink-cu12==12.8.93
71
+ nvidia-nvshmem-cu12==3.3.20
72
+ nvidia-nvtx-cu12==12.8.90
73
+ openai==2.14.0
74
+ orjson==3.11.5
75
+ ormsgpack==1.12.1
76
+ packaging==25.0
77
+ pandas==2.3.3
78
+ pillow==12.1.0
79
+ propcache==0.4.1
80
+ protobuf==6.33.2
81
+ pyarrow==22.0.0
82
+ pydantic==2.12.5
83
+ pydantic-core==2.41.5
84
+ pydantic-settings==2.12.0
85
+ pydeck==0.9.1
86
+ pypdf==6.5.0
87
+ python-dateutil==2.9.0.post0
88
+ python-dotenv==1.2.1
89
+ pytz==2025.2
90
+ pyyaml==6.0.3
91
+ referencing==0.37.0
92
+ regex==2025.11.3
93
+ requests==2.32.5
94
+ requests-toolbelt==1.0.0
95
+ rpds-py==0.30.0
96
+ safetensors==0.7.0
97
+ scikit-learn==1.8.0
98
+ scipy==1.16.3
99
+ sentence-transformers==5.2.0
100
+ setuptools==80.9.0
101
+ six==1.17.0
102
+ smmap==5.0.2
103
+ sniffio==1.3.1
104
+ soupsieve==2.8.1
105
+ sqlalchemy==2.0.45
106
+ streamlit==1.52.2
107
+ sympy==1.14.0
108
+ tavily-python==0.7.17
109
+ tenacity==9.1.2
110
+ threadpoolctl==3.6.0
111
+ tiktoken==0.12.0
112
+ tokenizers==0.22.1
113
+ toml==0.10.2
114
+ torch==2.9.1
115
+ tornado==6.5.4
116
+ tqdm==4.67.1
117
+ transformers==4.57.3
118
+ triton==3.5.1
119
+ typing-extensions==4.15.0
120
+ typing-inspect==0.9.0
121
+ typing-inspection==0.4.2
122
+ tzdata==2025.3
123
+ urllib3==2.6.2
124
+ uuid-utils==0.12.0
125
+ watchdog==6.0.0
126
+ xxhash==3.6.0
127
+ yarl==1.22.0
128
+ youtube-search==2.2.0
129
+ zstandard==0.25.0
web_app.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import json
4
+ import re
5
+ import datetime
6
+ import tempfile
7
+ # 导入 LangChain 相关组件
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain_core.prompts import ChatPromptTemplate
10
+ from langchain_core.output_parsers import StrOutputParser
11
+ from langchain_core.runnables import RunnableLambda
12
+ from langchain_core.tools import tool
13
+ from langchain_community.tools.tavily_search import TavilySearchResults
14
+ from langchain_community.document_loaders import PyPDFLoader
15
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
16
+ from langchain_huggingface import HuggingFaceEmbeddings
17
+ from langchain_community.vectorstores import FAISS
18
+ # 导入 YouTube 搜索库
19
+ from youtube_search import YoutubeSearch
20
+
21
+ # ==========================================
22
+ # 1. 基础配置与环境初始化
23
+ # ==========================================
24
+ # 配置 Streamlit 页面标题、图标和布局
25
+ st.set_page_config(page_title="FeiChat Final", page_icon="✨", layout="wide")
26
+ st.title("✨ FeiChat (Tavily + YouTube 完美版)")
27
+
28
+ # 设置 API Key
29
+ # 注意:实际生产中建议使用 st.secrets 或系统环境变量,不要直接写在代码里
30
+ os.environ["OPENAI_API_KEY"] = "lm-studio" # 指向本地 LM Studio,Key 随意填写
31
+ os.environ["TAVILY_API_KEY"] = "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" # Tavily 搜索引擎 Key
32
+
33
+ # 初始化 Session State (会话状态)
34
+ #用于在 Streamlit 页面刷新(rerun)时保存聊天记录和向量数据库
35
+ if "messages" not in st.session_state:
36
+ st.session_state.messages = [] # 存储对话历史
37
+ if "vector_store" not in st.session_state:
38
+ st.session_state.vector_store = None # 存储 PDF 向量索引
39
+
40
+ # ==========================================
41
+ # 1.1 模型加载 (使用缓存避免重复加载)
42
+ # ==========================================
43
+ @st.cache_resource
44
+ def get_models():
45
+ """
46
+ 初始化 LLM 和 Embedding 模型。
47
+ 使用 @st.cache_resource 装饰器,确保只加载一次,节省资源。
48
+ """
49
+ # 1. 路由模型 (Router):温度设为 0.0,要求输出精确,用于判断意图
50
+ router = ChatOpenAI(
51
+ base_url="http://127.0.0.1:1234/v1", # 连接本地 LM Studio 端口
52
+ model="kuaidao-c-suite-v2",
53
+ temperature=0.0
54
+ )
55
+
56
+ # 2. 对话模型 (Chat):温度设为 0.7,用于生成流畅、自然的回答,开启流式输出
57
+ chat = ChatOpenAI(
58
+ base_url="http://127.0.0.1:1234/v1",
59
+ model="kuaidao-c-suite-v2",
60
+ temperature=0.7,
61
+ streaming=True
62
+ )
63
+
64
+ # 3. 嵌入模型 (Embeddings):用于将 PDF 文本转化为向量,这里使用 HuggingFace 的轻量级模型
65
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
66
+
67
+ return router, chat, embeddings
68
+
69
+ llm_router, llm_chat, embeddings = get_models()
70
+
71
+ # ==========================================
72
+ # 2. 侧边栏 RAG (私有知识库处理)
73
+ # ==========================================
74
+ with st.sidebar:
75
+ st.header("📂 私有知识库")
76
+
77
+ # 文件上传控件
78
+ uploaded_file = st.file_uploader("上传 PDF (仅当问及文档内容时使用)", type=["pdf"])
79
+
80
+ # 如果用户上传了文件,且向量库还未建立,则开始处理
81
+ if uploaded_file and st.session_state.vector_store is None:
82
+ with st.status("正在学习文档...", expanded=True):
83
+ # 1. 创建临时文件保存上传的 PDF (PyPDFLoader 需要本地文件路径)
84
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
85
+ tmp.write(uploaded_file.read())
86
+ path = tmp.name
87
+
88
+ # 2. 加载 PDF
89
+ loader = PyPDFLoader(path)
90
+ docs = loader.load()
91
+
92
+ # 3. 文本切分:将长文档切成 500字符的小块,保留 50字符重叠以保持上下文
93
+ splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
94
+ splits = splitter.split_documents(docs)
95
+
96
+ # 4. 向量化并存储:使用 FAISS 构建向量索引
97
+ st.session_state.vector_store = FAISS.from_documents(splits, embeddings)
98
+ st.success(f"已索引 {len(splits)} 个片段")
99
+
100
+ # 5. 删除临时文件,保持整洁
101
+ os.remove(path)
102
+
103
+ # 清除记忆按钮
104
+ if st.button("🗑️ 清空记忆"):
105
+ st.session_state.messages = []
106
+ st.session_state.vector_store = None
107
+ st.rerun() # 重新运行脚本以刷新页面状态
108
+
109
+ # ==========================================
110
+ # 3. 工具定义 (Search & RAG)
111
+ # ==========================================
112
+ # 初始化 Tavily 搜索客户端
113
+ tavily_engine = TavilySearchResults(max_results=5)
114
+
115
+ @tool
116
+ def internet_search(query: str) -> str:
117
+ """Tavily 联网搜索工具函数,供 Agent 调用。"""
118
+ print(f"🕵️ Tavily 搜索: {query}") # 后台打印日志
119
+ try:
120
+ results = tavily_engine.invoke({"query": query})
121
+ formatted = []
122
+ # 格式化搜索结果,包含 URL 和��容摘要
123
+ for i, res in enumerate(results):
124
+ formatted.append(f"【来源】({res['url']}):\n{res['content']}")
125
+ return "\n\n".join(formatted)
126
+ except Exception as e:
127
+ return f"Error: {e}"
128
+
129
+ @tool
130
+ def knowledge_base_search(query: str) -> str:
131
+ """知识库(RAG)搜索工具函数。"""
132
+ # 如果没上传文件,直接返回提示
133
+ if st.session_state.vector_store is None: return "用户未上传任何文档。"
134
+
135
+ # 在向量库中搜索最相似的 3 个片段
136
+ docs = st.session_state.vector_store.similarity_search(query, k=3)
137
+ return "\n\n".join([f"【文档片段】: {d.page_content}" for d in docs])
138
+
139
+ # 将工具放入字典,方便 Router 调用
140
+ tools = {"internet_search": internet_search, "knowledge_base_search": knowledge_base_search}
141
+
142
+ def search_youtube(query):
143
+ """
144
+ YouTube 搜索辅助函数
145
+ 注意:这是独立功能,不作为 LLM 的 Tool,而是在 UI 层直接展示结果
146
+ """
147
+ try:
148
+ # 限制结果为 3 个
149
+ return YoutubeSearch(query, max_results=3).to_dict()
150
+ except: return []
151
+
152
+ # ==========================================
153
+ # 4. 核心逻辑 (Router & Chain)
154
+ # ==========================================
155
+ def get_time(): return datetime.datetime.now().strftime("%Y年%m月%d日")
156
+
157
+ # 4.1 意图识别 Prompt
158
+ # 作用:让 LLM 判断用户是想闲聊、查文档还是联网搜索,并提取搜索关键词
159
+ intent_prompt = ChatPromptTemplate.from_messages([
160
+ ("system", """
161
+ 你是一个智能路由。当前时间:{current_date}。
162
+
163
+ 【工具选择逻辑】:
164
+ 1. knowledge_base_search: 🔴 仅当用户明确提到“文档”、“PDF”、“上传的文件”等时使用。
165
+ 2. internet_search: 🟢 默认选项(如果用户问知识性问题)。
166
+ 3. CHAT: 仅用于纯打招呼、情感交流,不需要外部信息。
167
+
168
+ 【Query生成规则】:
169
+ - 强时效性问题(新闻、天气):必须在关键词中加 `{current_date}`。
170
+ - 弱时效性问题(歌曲、百科、人物):**禁止加日期**,直接用实体名。
171
+
172
+ 返回 JSON 格式: {{ "intent": "CHAT" 或 "TOOL", "tool_name": "...", "tool_args": {{ "query": "..." }} }}
173
+ """),
174
+ ("user", "历史:\n{chat_history}\n\n输入:\n{input}")
175
+ ])
176
+
177
+ def parse_router(text):
178
+ """解析 Router LLM 返回的 JSON 字符串"""
179
+ try:
180
+ # 使用正则提取 Markdown 代码块中的 JSON (防止 LLM 输出 ```json ... ```)
181
+ if "```" in text: text = re.search(r"```(?:json)?(.*?)```", text, re.DOTALL).group(1)
182
+ return json.loads(text.strip())
183
+ except:
184
+ # 解析失败则默认回退到纯聊天模式
185
+ return {"intent": "CHAT"}
186
+
187
+ # 构建路由链:Prompt -> LLM -> 文本解析 -> JSON解析
188
+ intent_chain = intent_prompt | llm_router | StrOutputParser() | RunnableLambda(parse_router)
189
+
190
+ # 4.2 最终回复润色 Prompt
191
+ # 作用:根据搜索结果生成给用户的最终回答
192
+ response_prompt = ChatPromptTemplate.from_messages([
193
+ ("system", """
194
+ 你是一个严谨的信息整合助手。
195
+ 请严格基于【搜索结果】回答。
196
+ 1. 如果结果里没有,诚实说“未找到”。
197
+ 2. 严禁捏造。
198
+ """),
199
+ ("user", "问题: {user_input}\n\n搜索结果:\n{tool_result}")
200
+ ])
201
+ response_chain = response_prompt | llm_chat | StrOutputParser()
202
+
203
+ # 4.3 纯闲聊 Prompt
204
+ chat_chain = ChatPromptTemplate.from_messages([
205
+ ("system", "助手。"), ("user", "{input}")
206
+ ]) | llm_chat | StrOutputParser()
207
+
208
+ # ==========================================
209
+ # 5. 界面 UI 交互逻辑
210
+ # ==========================================
211
+ # 5.1 显示历史消息
212
+ for msg in st.session_state.messages:
213
+ with st.chat_message(msg["role"]):
214
+ st.markdown(msg["content"])
215
+
216
+ # 5.2 处理用户输入
217
+ if user_input := st.chat_input("问:Fruits Zipper 最火的歌..."):
218
+ # 记录用户输入
219
+ st.session_state.messages.append({"role": "user", "content": user_input})
220
+ with st.chat_message("user"):
221
+ st.markdown(user_input)
222
+
223
+ with st.chat_message("assistant"):
224
+ # 使用 st.status 显示“思考中”状态动画
225
+ with st.status("🧠 思考中...", expanded=False) as status:
226
+ # 准备上下文
227
+ hist = str(st.session_state.messages[:-1])
228
+ now = get_time()
229
+
230
+ # --- 第一步:路由判断 ---
231
+ intent_res = intent_chain.invoke({"input": user_input, "chat_history": hist, "current_date": now})
232
+ st.json(intent_res) # 调试用:在折叠状态里显示路由结果
233
+
234
+ final_stream = None # 用于存储最终的流式输出对象
235
+ yt_query = None # 用于存储 YouTube 搜索关键词
236
+
237
+ # --- 第二步:根据意图分支 ---
238
+ if intent_res.get("intent") == "TOOL":
239
+ tool_name = intent_res.get("tool_name")
240
+ query = intent_res.get("tool_args", {}).get("query", user_input)
241
+
242
+ # 如果是联网搜索,顺便记录一���关键词用于稍后搜 YouTube
243
+ if tool_name == "internet_search":
244
+ yt_query = query
245
+
246
+ if tool_name in tools:
247
+ try:
248
+ # 执行工具 (Tavily 或 向量检索)
249
+ tool_res = tools[tool_name].invoke(query)
250
+
251
+ # 在 UI 中增加一个折叠框,显示搜索到的原始数据(增加可信度)
252
+ with st.expander("📄 查看搜索摘要"):
253
+ st.text(tool_res)
254
+
255
+ # 防幻觉逻辑:如果搜索结果太短或包含错误信息
256
+ if "未找到有效信息" in tool_res or len(tool_res.strip()) < 50:
257
+ final_stream = response_chain.stream({"user_input": user_input, "tool_result": "未找到相关信息。"})
258
+ else:
259
+ # 正常生成回答
260
+ final_stream = response_chain.stream({"user_input": user_input, "tool_result": tool_res})
261
+ except Exception as e:
262
+ st.error(f"工具执行失败: {e}")
263
+ else:
264
+ st.error("未找到工具")
265
+ else:
266
+ # 如果是 CHAT 意图,直接闲聊
267
+ final_stream = chat_chain.stream({"input": user_input})
268
+
269
+ # 更新状态栏为完成
270
+ status.update(label="完成", state="complete")
271
+
272
+ # --- 第三步:流式输出回答 ---
273
+ if final_stream:
274
+ full_response = st.write_stream(final_stream) # Streamlit 自带的打字机效果
275
+ st.session_state.messages.append({"role": "assistant", "content": full_response})
276
+
277
+ # --- 第四步:展示 YouTube 视频 (仅当触发了联网搜索时) ---
278
+ if yt_query:
279
+ st.markdown("---") # 分割线
280
+ videos = search_youtube(yt_query)
281
+ if videos:
282
+ cols = st.columns(3) # 三列布局
283
+ for i, v in enumerate(videos[:3]):
284
+ with cols[i]:
285
+ # 处理缩略图:有些 API 返回的是列表,有些是字符串,这里做了兼容处理
286
+ thumb = v['thumbnails'][0] if isinstance(v['thumbnails'], list) else v['thumbnails']
287
+
288
+ st.image(thumb, use_container_width=True)
289
+ # 显示标题链接
290
+ st.markdown(f"**[{v['title']}](https://www.youtube.com{v['url_suffix']})**")
291
+ # 显示观看量
292
+ st.caption(f"👀 {v['views']}")
293
+ else:
294
+ st.caption("未找到相关视频。")