Donlagon007 commited on
Commit
9af323d
·
verified ·
1 Parent(s): 82a5816

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +662 -0
  2. requirements.txt +17 -3
app.py ADDED
@@ -0,0 +1,662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+ import streamlit as st
5
+ import re
6
+ from typing import List, Dict, Any, Optional
7
+
8
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+ from langchain_community.vectorstores import FAISS
11
+ from langchain.chains import ConversationalRetrievalChain
12
+ from langchain.memory import ConversationBufferMemory
13
+ from langchain_community.document_loaders import PyPDFLoader, UnstructuredPDFLoader
14
+ from langchain.schema import Document
15
+ from langchain.prompts import PromptTemplate
16
+ from langchain.chains.question_answering import load_qa_chain
17
+ from langchain.chains.llm import LLMChain
18
+
19
+ # ==============================================
20
+ # 基本設定
21
+ # ==============================================
22
+ print("✅ LangChain 0.2.x environment OK!")
23
+ MODEL_NAME = os.getenv("LLM_MODEL", "gpt-4o") # 使用更強的模型
24
+
25
+
26
+ # ==============================================
27
+ # 通用查詢類型識別
28
+ # ==============================================
29
+ def identify_query_type(query: str) -> str:
30
+ """識別查詢的類型,以優化回答策略"""
31
+
32
+ # 檢測是否為繼續或完整內容請求
33
+ if re.search(
34
+ r'後面是什麼|後面要接什麼|下一句是什麼|接著是什麼|如何接龍|怎麼接下去|全部內容|完整的|繼續|接續|更多|全文',
35
+ query):
36
+ return "completion"
37
+
38
+ # 檢測是否為解釋意思的請求
39
+ if re.search(r'是什麼意思|什麼意思|的意思|啥意思|解釋|說明|代表什麼|定義|含義|詮釋', query):
40
+ return "explanation"
41
+
42
+ # 檢測是否為摘要請求
43
+ if re.search(r'摘要|總結|概述|簡述|大意|重點|主旨', query):
44
+ return "summary"
45
+
46
+ # 檢測是否為比較請求
47
+ if re.search(r'比較|差別|區別|不同|相似|共同點|差異', query):
48
+ return "comparison"
49
+
50
+ # 默認為一般問題
51
+ return "general"
52
+
53
+
54
+ def extract_keyword_from_query(query: str) -> str:
55
+ """從查詢中提取關鍵詞"""
56
+
57
+ # 處理完整內容類查詢
58
+ completion_pattern = r'(.+?)(?:後面是什麼|下一句是什麼|接著是什麼|如何接龍|怎麼接下去|繼續|接續|更多|全文)'
59
+ match = re.search(completion_pattern, query)
60
+ if match:
61
+ return match.group(1).strip()
62
+
63
+ # 處理解釋類查詢
64
+ explanation_pattern = r'(.+?)(?:是什麼意思|什麼意思|的意思|啥意思|解釋|說明|代表什麼|定義|含義|詮釋)'
65
+ match = re.search(explanation_pattern, query)
66
+ if match:
67
+ return match.group(1).strip()
68
+
69
+ # 處理摘要類查詢
70
+ summary_pattern = r'(.+?)(?:摘要|總結|概述|簡述|大意|重點|主旨)'
71
+ match = re.search(summary_pattern, query)
72
+ if match:
73
+ return match.group(1).strip()
74
+
75
+ # 處理比較類查詢
76
+ comparison_pattern = r'(.+?)(?:和|與|跟|同)(.+?)(?:比較|差別|區別|不同|相似|共同點|差異)'
77
+ match = re.search(comparison_pattern, query)
78
+ if match:
79
+ return f"{match.group(1).strip()} {match.group(2).strip()}"
80
+
81
+ # 如果都沒匹配到,返回整個查詢作為關鍵詞
82
+ return query
83
+
84
+
85
+ def preprocess_query(query: str, topic_name: str = "") -> str:
86
+ """預處理查詢,優化檢索效果"""
87
+ query_type = identify_query_type(query)
88
+ keyword = extract_keyword_from_query(query)
89
+
90
+ # 根據查詢類型調整查詢字串,提升檢索效果
91
+ if query_type == "completion":
92
+ return f"完整內容 {keyword}"
93
+ elif query_type == "explanation":
94
+ return f"解釋 {keyword}"
95
+ elif query_type == "summary":
96
+ return f"摘要 {keyword}"
97
+ elif query_type == "comparison":
98
+ return f"比較 {keyword}"
99
+ else:
100
+ return query
101
+
102
+
103
+ # ==============================================
104
+ # 通用文本處理
105
+ # ==============================================
106
+ def process_text(text: str) -> str:
107
+ """處理文本,標準化格式"""
108
+ # 移除多餘空格
109
+ text = re.sub(r'\s+', ' ', text).strip()
110
+ # 將常見的章節標記轉換為標準格式,便於分割和檢索
111
+ text = re.sub(r'第[一二三四五六七八九十\d]+章', '## ', text)
112
+ text = re.sub(r'第[一二三四五六七八九十\d]+節', '### ', text)
113
+ return text
114
+
115
+
116
+ # ==============================================
117
+ # 優化的PDF加載函數
118
+ # ==============================================
119
+ def load_pdf_with_fallback(file_path: str) -> List[Document]:
120
+ """嘗試使用多種PDF加載器來確保中文內容被正確提取"""
121
+ try:
122
+ # 先嘗試 PyPDFLoader
123
+ docs = PyPDFLoader(file_path).load()
124
+
125
+ # 檢查提取的文本是否有效
126
+ if docs and any(len(doc.page_content.strip()) > 50 for doc in docs):
127
+ return docs
128
+ except Exception as e:
129
+ st.error(f"PyPDFLoader 載入失敗: {str(e)}")
130
+
131
+ try:
132
+ # 嘗試使用 UnstructuredPDFLoader 作為後備
133
+ docs = UnstructuredPDFLoader(file_path).load()
134
+ return docs
135
+ except Exception as e:
136
+ st.error(f"UnstructuredPDFLoader 載入失敗: {str(e)}")
137
+ raise ValueError(f"無法載入PDF: {str(e)}")
138
+
139
+
140
+ # ==============================================
141
+ # 建立向量索引(優化中文處理)
142
+ # ==============================================
143
+ @st.cache_resource(show_spinner=False)
144
+ def build_retriever_from_files(uploaded_files):
145
+ """從上傳的PDF文件建立優化的檢索器"""
146
+
147
+ tmpdir = tempfile.TemporaryDirectory()
148
+ tmp_paths = []
149
+
150
+ # 儲存上傳的檔案
151
+ for f in uploaded_files:
152
+ p = Path(tmpdir.name) / f.name
153
+ with open(p, "wb") as out:
154
+ out.write(f.read())
155
+ tmp_paths.append(str(p))
156
+
157
+ # 載入所有文檔
158
+ all_docs = []
159
+ for p in tmp_paths:
160
+ docs = load_pdf_with_fallback(p)
161
+
162
+ # 優化文本處理
163
+ for doc in docs:
164
+ doc.page_content = process_text(doc.page_content)
165
+
166
+ all_docs.extend(docs)
167
+
168
+ # 通用文本的優化切分器 - 適合中文
169
+ splitter = RecursiveCharacterTextSplitter(
170
+ chunk_size=800, # 通用文本的建議大小
171
+ chunk_overlap=150, # 適當的重疊以確保上下文
172
+ separators=["##", "###", "\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
173
+ )
174
+
175
+ # 切分文檔
176
+ chunks = splitter.split_documents(all_docs)
177
+
178
+ # 使用OpenAI嵌入模型
179
+ embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
180
+
181
+ # 構建向量存儲
182
+ vs = FAISS.from_documents(chunks, embeddings)
183
+
184
+ # 返回檢索器
185
+ return vs.as_retriever(search_kwargs={"k": 4}) # 增加檢索文檔數量以提高覆蓋率
186
+
187
+
188
+ # ==============================================
189
+ # 預設的領域提示模板
190
+ # ==============================================
191
+
192
+ # 通用提示模板
193
+ DEFAULT_SYSTEM_PROMPT = """你是一位專業的知識助手,專精於{topic}相關知識。請根據以下文檔內容回答問題。
194
+
195
+ 請注意以下特殊情況:
196
+ 1. 如果問題是關於完整內容或繼續閱讀,請提供完整信息。
197
+ 2. 如果問題是關於解釋意思,請詳細解釋含義、用法和背景。
198
+ 3. 如果問題是關於摘要,請提煉出最重要的概念和要點。
199
+ 4. 如果問題是關於比較,請清晰列出相似點和不同點。
200
+ 5. 即使問題可能不完整或模糊,也請盡量根據提供的信息回答。
201
+
202
+ 請用繁體中文回答,語氣親切專業。如果找不到確切答案,請說明無法找到相關資訊,而不是簡單回答「我不知道」。"""
203
+
204
+ # 醫學領域的系統提示
205
+ MEDICAL_SYSTEM_PROMPT = """你是一位醫學專家,專精於{topic}相關醫學知識。請根據以下文檔內容回答醫學相關問題。
206
+
207
+ 在回答時請遵循以下原則:
208
+ 1. 保持專業準確性,使用醫學術語但同時確保病人或普通人可以理解。
209
+ 2. 區分已確立的醫學共識與仍有爭議的領域。
210
+ 3. 對於病症解釋,提供清晰的定義、可能的原因、典型症狀和標準治療方法。
211
+ 4. 對於藥物相關問題,說明作用機制、常見副作用和用藥注意事項。
212
+ 5. 你是基於專業個人化建議,但病人病況嚴重時得適時提醒本建議不能替代專業醫生的診斷和建議。
213
+
214
+ 請用繁體中文回答,語氣專業但平易近人。如果找不到確切答案,請明確說明而不是猜測。"""
215
+
216
+ # 法律領域的系統提示
217
+ LEGAL_SYSTEM_PROMPT = """你是一位法律專家,專精於{topic}相關法律知識。請根據以下文檔內容回答法律相關問題。
218
+
219
+ 在回答時請遵循以下原則:
220
+ 1. 清晰界定法律概念和術語。
221
+ 2. 引述相關法條或判例時,提供準確的出處。
222
+ 3. 區分法律事實與法律見解。
223
+ 4. 說明不同情況下可能適用的不同法律解釋。
224
+ 5. 提醒使用者,你的回答僅供參考,不構成法律建議,複雜法律問題應諮詢執業律師。
225
+
226
+ 請用繁體中文回答,語氣專業嚴謹。如果找不到確切答案,請明確說明而不是提供不確定的法律意見。"""
227
+
228
+ # 技術領域的系統提示
229
+ TECH_SYSTEM_PROMPT = """你是一位技術專家,專精於{topic}相關技術知識。請根據以下文檔內容回答技術相關問題。
230
+
231
+ 在回答時請遵循以下原則:
232
+ 1. 提供簡潔明確的技術解釋,適當包含代碼示例。
233
+ 2. 區分核心概念和實現細節。
234
+ 3. 說明技術選擇的優缺點和適用場景。
235
+ 4. 關注最佳實踐和常見的錯誤模式。
236
+ 5. 適當引入類比或視覺化解釋,幫助理解複雜概念。
237
+
238
+ 請用繁體中文回答,語氣專業但平易近人。如果找不到確切答案,請明確說明而不是提供猜測性的技術建議。"""
239
+
240
+ # 教育領域的系統提示
241
+ EDUCATION_SYSTEM_PROMPT = """你是一位教育專家,專精於{topic}相關教育���識。請根據以下文檔內容回答教育相關問題。
242
+
243
+ 在回答時請遵循以下原則:
244
+ 1. 適應不同學習階段學生的需求,提供適齡的解釋。
245
+ 2. 結合具體例子和情境來解釋抽象概念。
246
+ 3. 關注學習目標、評估方法和教學策略。
247
+ 4. 鼓勵批判性思考和自主學習。
248
+ 5. 考慮不同學習風格和能力的差異化教學方法。
249
+
250
+ 請用繁體中文回答,語氣親切鼓勵。如果找不到確切答案,請建議可能的替代學習資源或方向。"""
251
+
252
+ # 金融領域的系統提示
253
+ FINANCE_SYSTEM_PROMPT = """你是一位金融專家,專精於{topic}相關金融知識。請根據以下文檔內容回答金融相關問題。
254
+
255
+ 在回答時請遵循以下原則:
256
+ 1. 清晰解釋金融概念、產品和服務的特點。
257
+ 2. 討論金融決策時,考慮風險、收益和時間因素。
258
+ 3. 使用適當的數據和指標來支持分析。
259
+ 4. 關注市場趨勢和經濟環境的影響。
260
+ 5. 提醒使用者,金融決策應基於個人情況,並可能需要專業顧問的建議。
261
+
262
+ 請用繁體中文回答,語氣專業客觀。如果找不到確切答案,請明確說明而不是提供可能誤導的金融建議。"""
263
+
264
+ # 歷史領域的系統提示
265
+ HISTORY_SYSTEM_PROMPT = """你是一位歷史專家,專精於{topic}相關歷史知識。請根據以下文檔內容回答歷史相關問題。
266
+
267
+ 在回答時請遵循以下原則:
268
+ 1. 提供準確的年代、人物和事件資訊。
269
+ 2. 區分歷史事實與歷史解釋或觀點。
270
+ 3. 考慮歷史事件的背景、原因和影響。
271
+ 4. 呈現多元的歷史視角,尤其是不同文化或群體的觀點。
272
+ 5. 適當引用歷史資料來源或學術研究。
273
+
274
+ 請用繁體中文回答,語氣學術但生動。如果找不到確切答案,請明確說明歷史記錄的局限性。"""
275
+
276
+
277
+ # ==============================================
278
+ # 獲取領域提示
279
+ # ==============================================
280
+ def get_domain_prompt(domain: str, topic: str) -> str:
281
+ """根據領域返回適當的系統提示模板"""
282
+ domain_prompts = {
283
+ "通用": DEFAULT_SYSTEM_PROMPT,
284
+ "醫學": MEDICAL_SYSTEM_PROMPT,
285
+ "法律": LEGAL_SYSTEM_PROMPT,
286
+ "技術": TECH_SYSTEM_PROMPT,
287
+ "教育": EDUCATION_SYSTEM_PROMPT,
288
+ "金融": FINANCE_SYSTEM_PROMPT,
289
+ "歷史": HISTORY_SYSTEM_PROMPT
290
+ }
291
+
292
+ prompt_template = domain_prompts.get(domain, DEFAULT_SYSTEM_PROMPT)
293
+ return prompt_template.format(topic=topic)
294
+
295
+
296
+ # ==============================================
297
+ # 優化的提示模板
298
+ # ==============================================
299
+ def create_qa_prompt(system_prompt: str):
300
+ """根據系統提示創建QA提示模板"""
301
+ template = f"""{system_prompt}
302
+
303
+ 文檔內容:
304
+ {{context}}
305
+
306
+ 問題: {{question}}
307
+
308
+ 回答:"""
309
+
310
+ return PromptTemplate(
311
+ template=template,
312
+ input_variables=["context", "question"]
313
+ )
314
+
315
+
316
+ # ==============================================
317
+ # 建立對話鏈
318
+ # ==============================================
319
+ @st.cache_resource(show_spinner=False)
320
+ def make_chain(_retriever, system_prompt: str, topic: str):
321
+ """建立針對特定主題和系統提示優化的對話鏈"""
322
+
323
+ # 使用更強的模型
324
+ llm = ChatOpenAI(
325
+ model=MODEL_NAME,
326
+ temperature=0,
327
+ )
328
+
329
+ # 使用自訂系統提示的提示模板
330
+ qa_prompt = create_qa_prompt(system_prompt)
331
+
332
+ # 自訂的問題凝練提示
333
+ condense_prompt = PromptTemplate.from_template(f"""
334
+ 根據以下對話歷史和最新的問題,生成一個獨立的問題,該問題應包含所有必要的上下文信息,以便於找到與{topic}相關的答案。
335
+
336
+ 對話歷史:
337
+ {{chat_history}}
338
+
339
+ 最新問題: {{question}}
340
+
341
+ 獨立問題:
342
+ """)
343
+
344
+ # 記憶模組設定
345
+ memory = ConversationBufferMemory(
346
+ memory_key="chat_history",
347
+ input_key="question",
348
+ output_key="answer",
349
+ return_messages=True
350
+ )
351
+
352
+ # 創建問題生成器
353
+ question_generator = LLMChain(
354
+ llm=llm,
355
+ prompt=condense_prompt
356
+ )
357
+
358
+ # 使用自定義問答鏈
359
+ qa_chain = load_qa_chain(
360
+ llm=llm,
361
+ chain_type="stuff",
362
+ prompt=qa_prompt,
363
+ verbose=True
364
+ )
365
+
366
+ # 建立對話檢索鏈
367
+ chain = ConversationalRetrievalChain(
368
+ retriever=_retriever,
369
+ combine_docs_chain=qa_chain,
370
+ question_generator=question_generator,
371
+ memory=memory,
372
+ return_source_documents=True,
373
+ verbose=True,
374
+ output_key="answer"
375
+ )
376
+
377
+ return chain
378
+
379
+
380
+ # ==============================================
381
+ # 處理查詢的主函數
382
+ # ==============================================
383
+ def process_query(query: str, chain, topic_name: str) -> Dict[str, Any]:
384
+ """處理用戶查詢,返回結果和來源"""
385
+
386
+ # 預處理查詢以優化檢索效果
387
+ processed_query = preprocess_query(query, topic_name)
388
+
389
+ # 使用對話鏈處理查詢
390
+ result = chain({"question": processed_query})
391
+
392
+ return result
393
+
394
+
395
+ # ==============================================
396
+ # Streamlit App UI
397
+ # ==============================================
398
+ def main():
399
+ # 初始化自定義設定
400
+ if "custom_settings" not in st.session_state:
401
+ st.session_state.custom_settings = {
402
+ "topic_name": "通用知識庫",
403
+ "expert_role": "知識專家",
404
+ "domain": "通用",
405
+ "custom_system_prompt": ""
406
+ }
407
+
408
+ st.set_page_config(
409
+ page_title="通用RAG問答系統",
410
+ page_icon="📚",
411
+ layout="wide"
412
+ )
413
+
414
+ st.title("📚 通用RAG智慧問答系統")
415
+ st.caption("上傳PDF → 建立索引 → 與文件對話")
416
+
417
+ with st.sidebar:
418
+ st.markdown("### 系統設定")
419
+ st.write(f"使用的模型:{MODEL_NAME}")
420
+
421
+ # 自定義主題名稱
422
+ topic_name = st.text_input(
423
+ "知識庫主題名稱",
424
+ value=st.session_state.custom_settings["topic_name"]
425
+ )
426
+
427
+ # 專家角色
428
+ expert_role = st.text_input(
429
+ "AI 助手角色",
430
+ value=st.session_state.custom_settings["expert_role"]
431
+ )
432
+
433
+ # 領域選擇
434
+ domain_options = ["通用", "醫學", "法律", "技術", "教育", "金融", "歷史", "自訂"]
435
+ selected_domain = st.selectbox(
436
+ "專業領域",
437
+ options=domain_options,
438
+ index=domain_options.index(st.session_state.custom_settings.get("domain", "通用"))
439
+ )
440
+
441
+ # 自訂系統提示
442
+ custom_system_prompt = st.session_state.custom_settings.get("custom_system_prompt", "")
443
+
444
+ if selected_domain == "自訂":
445
+ st.markdown("### 自訂系統提示 (System Prompt)")
446
+ custom_system_prompt = st.text_area(
447
+ "請輸入自訂的系統提示",
448
+ value=custom_system_prompt,
449
+ height=300,
450
+ help="這裡的提示將指導AI如何回答問題,可以定義角色、回答風格和專業領域特性"
451
+ )
452
+ else:
453
+ # 顯示預設提示供參考
454
+ with st.expander(f"查看{selected_domain}領域的系統提示"):
455
+ domain_prompt = get_domain_prompt(selected_domain, topic_name)
456
+ st.code(domain_prompt)
457
+
458
+ # 保存設定
459
+ settings_changed = (
460
+ topic_name != st.session_state.custom_settings["topic_name"] or
461
+ expert_role != st.session_state.custom_settings["expert_role"] or
462
+ selected_domain != st.session_state.custom_settings.get("domain", "通用") or
463
+ (selected_domain == "自訂" and custom_system_prompt != st.session_state.custom_settings.get(
464
+ "custom_system_prompt", ""))
465
+ )
466
+
467
+ if settings_changed:
468
+ st.session_state.custom_settings["topic_name"] = topic_name
469
+ st.session_state.custom_settings["expert_role"] = expert_role
470
+ st.session_state.custom_settings["domain"] = selected_domain
471
+
472
+ if selected_domain == "自訂":
473
+ st.session_state.custom_settings["custom_system_prompt"] = custom_system_prompt
474
+
475
+ # 如果設定變更且已有對話鏈,將重設鏈標記設為True
476
+ if "chain" in st.session_state and "retriever" in st.session_state:
477
+ st.session_state.reset_chain = True
478
+
479
+ # 清除對話歷史
480
+ if "messages" in st.session_state:
481
+ st.session_state.pop("messages", None)
482
+
483
+ # 檢查API KEY
484
+ api = os.getenv("OPENAI_API_KEY")
485
+ if not api:
486
+ st.error("請先在環境變數設定 OPENAI_API_KEY")
487
+ st.code("export OPENAI_API_KEY=你的OpenAI API金鑰")
488
+
489
+ # 檔案上傳區
490
+ uploaded = st.file_uploader(
491
+ "上傳PDF",
492
+ type=["pdf"],
493
+ accept_multiple_files=True,
494
+ help="上傳PDF文件"
495
+ )
496
+
497
+ # 清空對話按鈕
498
+ if "messages" in st.session_state:
499
+ st.button(
500
+ "🧹 清空對話",
501
+ on_click=lambda: st.session_state.pop("messages", None)
502
+ )
503
+
504
+ # 確定要使用的系統提示
505
+ if st.session_state.custom_settings["domain"] == "自訂":
506
+ system_prompt = st.session_state.custom_settings.get("custom_system_prompt", DEFAULT_SYSTEM_PROMPT)
507
+ else:
508
+ system_prompt = get_domain_prompt(
509
+ st.session_state.custom_settings["domain"],
510
+ st.session_state.custom_settings["topic_name"]
511
+ )
512
+
513
+ # 當有文件上傳且尚未建立對話鏈時
514
+ if uploaded and ("chain" not in st.session_state or "reset_chain" in st.session_state):
515
+ if "reset_chain" in st.session_state:
516
+ del st.session_state.reset_chain
517
+
518
+ with st.spinner(f"正在為「{topic_name}」建立文檔索引,這可能需要一點時間..."):
519
+ try:
520
+ # 建立檢索器
521
+ retriever = build_retriever_from_files(uploaded)
522
+ st.session_state.retriever = retriever
523
+
524
+ # 建立對話鏈
525
+ st.session_state.chain = make_chain(
526
+ retriever,
527
+ system_prompt,
528
+ st.session_state.custom_settings["topic_name"]
529
+ )
530
+
531
+ # 初始化訊息歷史
532
+ st.session_state.messages = []
533
+
534
+ st.success(f"「{topic_name}」知識庫索引建立成功!您現在可以開始提問了。")
535
+ except Exception as e:
536
+ st.error(f"索引建立失敗: {str(e)}")
537
+
538
+ # 提供範例問題,根據主題和領域動態生成
539
+ if "chain" in st.session_state and not st.session_state.messages:
540
+ domain = st.session_state.custom_settings.get("domain", "通用")
541
+
542
+ examples = {
543
+ "通用": [
544
+ f"{topic_name}中的重要概念是什麼?",
545
+ f"請解釋{topic_name}中的關鍵術語是什麼意思?",
546
+ f"請提供{topic_name}的摘要。",
547
+ f"{topic_name}的第一章內容是什麼?"
548
+ ],
549
+ "醫學": [
550
+ f"{topic_name}的病因是什麼?",
551
+ f"{topic_name}的診斷標準是什麼?",
552
+ f"{topic_name}和相似疾病的區別是什麼?",
553
+ f"請解釋{topic_name}的治療方法和注意事項。"
554
+ ],
555
+ "法律": [
556
+ f"{topic_name}的法律定義是什麼?",
557
+ f"{topic_name}相關法條的適用範圍?",
558
+ f"{topic_name}在不同情況下如何適用?",
559
+ f"{topic_name}涉及的法律責任有哪些?"
560
+ ],
561
+ "技術": [
562
+ f"{topic_name}的核心原理是什麼?",
563
+ f"{topic_name}在實際應用中的最佳實踐?",
564
+ f"{topic_name}相比其他技術有什麼優缺點?",
565
+ f"如何解決{topic_name}中常見的技術問題?"
566
+ ]
567
+ }
568
+
569
+ # 如果沒有特定領域的範例,使用通用範例
570
+ domain_examples = examples.get(domain, examples["通用"])
571
+
572
+ with st.expander("💡 範例問題", expanded=True):
573
+ examples_md = f"嘗試提問以下關於「{topic_name}」的問題:\n\n"
574
+ for example in domain_examples:
575
+ examples_md += f"- {example}\n"
576
+ st.markdown(examples_md)
577
+
578
+ # 建立雙欄佈局
579
+ col1, col2 = st.columns([2, 1])
580
+
581
+ with col1:
582
+ # 顯示對話歷史
583
+ if "messages" in st.session_state:
584
+ for role, content in st.session_state.messages:
585
+ with st.chat_message(role):
586
+ st.markdown(content)
587
+
588
+ # 使用者輸入
589
+ placeholder_text = f"請問關於{topic_name}的問題..." if "chain" in st.session_state else "請先上傳PDF文件..."
590
+ prompt = st.chat_input(placeholder_text, disabled="chain" not in st.session_state)
591
+
592
+ # 處理使用者提問
593
+ if prompt and "chain" in st.session_state:
594
+ # 添加使用者訊息到對話歷史
595
+ st.session_state.messages.append(("user", prompt))
596
+
597
+ # 顯示使用者訊息
598
+ with st.chat_message("user"):
599
+ st.markdown(prompt)
600
+
601
+ # 顯示助手回應
602
+ with st.chat_message("assistant"):
603
+ with st.spinner(f"思考{topic_name}相關問題中..."):
604
+ try:
605
+ # 處理查詢
606
+ result = process_query(
607
+ prompt,
608
+ st.session_state.chain,
609
+ st.session_state.custom_settings["topic_name"]
610
+ )
611
+ answer = result["answer"]
612
+
613
+ # 顯示答案
614
+ st.markdown(answer)
615
+
616
+ # 保存源文檔以供右側面板顯示
617
+ st.session_state.current_sources = result.get("source_documents", [])
618
+ except Exception as e:
619
+ error_msg = f"處理問題時出錯: {str(e)}"
620
+ st.error(error_msg)
621
+ answer = "抱歉,處理您的問題時出現了錯誤。請再試一次。"
622
+ st.session_state.current_sources = []
623
+
624
+ # 添加助手回應到對話歷史
625
+ st.session_state.messages.append(("assistant", answer))
626
+
627
+ # 右側面板顯示源文檔和設置
628
+ with col2:
629
+ # 顯示當前系統提示概要
630
+ st.markdown(f"### ⚙️ 當前配置")
631
+ st.info(f"""
632
+ **主題**: {topic_name}
633
+ **角色**: {expert_role}
634
+ **領域**: {st.session_state.custom_settings.get('domain', '通用')}
635
+ """)
636
+
637
+ st.markdown("### 💡 參考來源")
638
+
639
+ if "chain" in st.session_state:
640
+ if "current_sources" in st.session_state and st.session_state.current_sources:
641
+ for i, doc in enumerate(st.session_state.current_sources, 1):
642
+ with st.expander(f"來源 {i}"):
643
+ # 顯示文件名和頁碼
644
+ src = Path(doc.metadata.get("source", "?")).name
645
+ page = doc.metadata.get("page", None)
646
+ st.write(f"**文件**: {src}" + (f" · 第{page + 1}頁" if page is not None else ""))
647
+
648
+ # 顯示參考段落
649
+ st.markdown(f"**內容片段**:")
650
+ st.markdown(f"```\n{doc.page_content[:300]}...\n```")
651
+ else:
652
+ st.info("提問後將顯示參考來源")
653
+ else:
654
+ st.info("請先上傳PDF文件")
655
+
656
+ # 如果尚未上傳任何文件
657
+ if not uploaded:
658
+ st.info("👆 請先在側邊欄上傳PDF文件")
659
+
660
+
661
+ if __name__ == "__main__":
662
+ main()
requirements.txt CHANGED
@@ -1,3 +1,17 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ openai
3
+ tiktoken
4
+
5
+ langchain
6
+ langchain-core
7
+ langchain-community
8
+ langchain-openai
9
+
10
+ faiss-cpu
11
+ pypdf
12
+
13
+ # ถ้าจะใช้ Unstructured เป็น fallback (อาจ build ช้าขึ้น)
14
+ unstructured
15
+ unstructured[pdf]
16
+ python-magic
17
+ Pillow