mikao007 commited on
Commit
c242ec7
·
verified ·
1 Parent(s): 3bbf387

Upload G_L_RAG.py

Browse files
Files changed (1) hide show
  1. G_L_RAG.py +697 -0
G_L_RAG.py ADDED
@@ -0,0 +1,697 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ import gradio as gr
4
+ from PyPDF2 import PdfReader
5
+ import google.generativeai as genai
6
+ from langchain.text_splitter import CharacterTextSplitter
7
+ from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI
8
+ from langchain_community.vectorstores import FAISS
9
+ from langchain.chains.question_answering import load_qa_chain
10
+ from langchain.prompts import PromptTemplate
11
+ import shutil
12
+ import tempfile
13
+ from docx import Document
14
+ from docx.shared import Inches
15
+ from datetime import datetime
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # 延後讀取 API 金鑰:提供工具函式,實際需要時才讀取
21
+ def _get_api_key() -> str:
22
+ candidate_keys = [
23
+ "GOOGLE_API_KEY",
24
+ "GEMINI_API_KEY",
25
+ "GOOGLE_GENAI_API_KEY",
26
+ "GENAI_API_KEY",
27
+ ]
28
+ for key_name in candidate_keys:
29
+ value = os.getenv(key_name, "").strip()
30
+ if value:
31
+ # 同步一份到 GOOGLE_API_KEY 以相容底層套件
32
+ os.environ["GOOGLE_API_KEY"] = value
33
+ return value
34
+ return ""
35
+
36
+ class PDFChatBot:
37
+ def __init__(self):
38
+ self.vector_store = None
39
+ # 嵌入模型延後初始化,直到真的需要(處理或載入向量庫)
40
+ self.embeddings = None
41
+ self.processed_files = []
42
+ self.chat_history = [] # 儲存聊天歷史
43
+
44
+ def get_pdf_text(self, pdf_files):
45
+ """從多個PDF文件中提取文字"""
46
+ raw_text = ""
47
+ processed_count = 0
48
+
49
+ if not pdf_files:
50
+ return raw_text, processed_count
51
+
52
+ # 處理單個文件和多個文件
53
+ if not isinstance(pdf_files, list):
54
+ pdf_files = [pdf_files]
55
+
56
+ for pdf_file in pdf_files:
57
+ try:
58
+ # 如果是上傳的文件對象,使用其name屬性
59
+ pdf_path = pdf_file.name if hasattr(pdf_file, 'name') else pdf_file
60
+
61
+ pdf_reader = PdfReader(pdf_path)
62
+ file_text = ""
63
+ for page in pdf_reader.pages:
64
+ text = page.extract_text()
65
+ if text:
66
+ file_text += text + "\n"
67
+
68
+ if file_text.strip():
69
+ raw_text += file_text
70
+ processed_count += 1
71
+ self.processed_files.append(os.path.basename(pdf_path))
72
+
73
+ except Exception as e:
74
+ print(f"讀取PDF時發生錯誤:{str(e)}")
75
+ continue
76
+
77
+ return raw_text, processed_count
78
+
79
+ def get_pdf_text_via_gemini(self, pdf_files):
80
+ """使用 Gemini 2.0 Flash 直接解析 PDF 文字(透過 Files API)。"""
81
+ api_key = _get_api_key()
82
+ if not api_key:
83
+ return "", 0
84
+
85
+ genai.configure(api_key=api_key)
86
+ model = genai.GenerativeModel("gemini-2.0-flash-exp")
87
+
88
+ raw_text = ""
89
+ processed_count = 0
90
+
91
+ if not pdf_files:
92
+ return raw_text, processed_count
93
+
94
+ if not isinstance(pdf_files, list):
95
+ pdf_files = [pdf_files]
96
+
97
+ for pdf_file in pdf_files:
98
+ try:
99
+ pdf_path = pdf_file.name if hasattr(pdf_file, 'name') else pdf_file
100
+ uploaded = genai.upload_file(pdf_path)
101
+ prompt = (
102
+ "請從此 PDF 中提取可讀文字,按頁面順序輸出純文字。"
103
+ )
104
+ resp = model.generate_content([uploaded, prompt])
105
+ text = resp.text or ""
106
+ if text.strip():
107
+ raw_text += text + "\n"
108
+ processed_count += 1
109
+ self.processed_files.append(os.path.basename(pdf_path))
110
+ except Exception as e:
111
+ print(f"使用Gemini解析PDF時發生錯誤:{str(e)}")
112
+ continue
113
+
114
+ return raw_text, processed_count
115
+
116
+ def get_text_chunks(self, text):
117
+ """將文字分割成區塊進行處理"""
118
+ text_splitter = CharacterTextSplitter(
119
+ separator="\n",
120
+ chunk_size=10000,
121
+ chunk_overlap=1000,
122
+ length_function=len
123
+ )
124
+ chunks = text_splitter.split_text(text)
125
+ return chunks
126
+
127
+ def create_vector_store(self, chunks):
128
+ """從文字區塊創建FAISS向量存儲"""
129
+ try:
130
+ if self.embeddings is None:
131
+ api_key = _get_api_key()
132
+ if not api_key:
133
+ return False
134
+ self.embeddings = GoogleGenerativeAIEmbeddings(
135
+ model="models/text-embedding-004",
136
+ google_api_key=api_key
137
+ )
138
+ self.vector_store = FAISS.from_texts(chunks, self.embeddings)
139
+ self.vector_store.save_local("faiss_index")
140
+ return True
141
+ except Exception as e:
142
+ print(f"創建向量存儲時發生錯誤:{str(e)}")
143
+ return False
144
+
145
+ def load_vector_store(self):
146
+ """載入已存在的向量存儲"""
147
+ try:
148
+ if os.path.exists("faiss_index"):
149
+ if self.embeddings is None:
150
+ api_key = _get_api_key()
151
+ if not api_key:
152
+ return False
153
+ self.embeddings = GoogleGenerativeAIEmbeddings(
154
+ model="models/text-embedding-004",
155
+ google_api_key=api_key
156
+ )
157
+ self.vector_store = FAISS.load_local(
158
+ "faiss_index",
159
+ embeddings=self.embeddings,
160
+ allow_dangerous_deserialization=True
161
+ )
162
+ return True
163
+ else:
164
+ return False
165
+ except Exception as e:
166
+ print(f"載入向量存儲時發生錯誤:{str(e)}")
167
+ return False
168
+
169
+ def get_conversational_chain(self, temperature=0.3, max_tokens=4096):
170
+ """創建對話鏈"""
171
+ prompt_template = """
172
+ 根據提供的內容盡可能詳細地回答問題。確保提供所有細節。
173
+ 如果你需要更多細節來完美回答問題,那麼請詢問你認為需要了解的更多細節。
174
+ 如果答案不在提供的內容中,只需說"在您提供的內容中找不到答案"。不要提供錯誤的答案。
175
+
176
+ 內容:\n {context}\n
177
+ 問題: \n{question}\n
178
+
179
+ 回答:
180
+ """
181
+
182
+ # Using Flash 2.0 model(延後讀取 API Key)
183
+ api_key = _get_api_key()
184
+ if not api_key:
185
+ raise RuntimeError("尚未設定 API 金鑰,請於部署後設定 GOOGLE_API_KEY 再重試。")
186
+
187
+ model = ChatGoogleGenerativeAI(
188
+ model="gemini-2.0-flash-exp",
189
+ google_api_key=api_key,
190
+ temperature=temperature,
191
+ max_tokens=max_tokens,
192
+ top_p=0.8,
193
+ top_k=40
194
+ )
195
+
196
+ prompt = PromptTemplate(
197
+ template=prompt_template,
198
+ input_variables=['context', 'question']
199
+ )
200
+
201
+ chain = load_qa_chain(model, chain_type="stuff", prompt=prompt)
202
+ return chain
203
+
204
+ def answer_question(self, question, temperature=0.3, max_tokens=4096, search_k=6):
205
+ """回答用戶問題"""
206
+ if not self.vector_store:
207
+ return "請先上傳並處理PDF文件!"
208
+
209
+ if not question.strip():
210
+ return "請輸入您的問題。"
211
+
212
+ try:
213
+ # 搜索相關文檔
214
+ docs = self.vector_store.similarity_search(question, k=search_k)
215
+
216
+ if not docs:
217
+ return "在上傳的文檔中找不到相關信息。"
218
+
219
+ # 生成回答
220
+ chain = self.get_conversational_chain(temperature, max_tokens)
221
+ response = chain(
222
+ {
223
+ "input_documents": docs,
224
+ "question": question,
225
+ },
226
+ return_only_outputs=True
227
+ )
228
+
229
+ return response["output_text"]
230
+
231
+ except Exception as e:
232
+ return f"處理問題時發生錯誤:{str(e)}"
233
+
234
+ def process_pdfs(self, pdf_files, progress=gr.Progress(), use_gemini=False):
235
+ """處理PDF文件"""
236
+ if not pdf_files:
237
+ return "請上傳至少一個PDF文件。", ""
238
+
239
+ self.processed_files = []
240
+ progress(0, desc="開始處理PDF文件...")
241
+
242
+ # 提取文字
243
+ progress(0.2, desc="提取PDF文字內容...")
244
+ if use_gemini:
245
+ raw_text, processed_count = self.get_pdf_text_via_gemini(pdf_files)
246
+ else:
247
+ raw_text, processed_count = self.get_pdf_text(pdf_files)
248
+
249
+ if not raw_text.strip():
250
+ return "無法從PDF文件中提取到文字。", ""
251
+
252
+ progress(0.4, desc="分割文字內容...")
253
+ # 分割文字
254
+ text_chunks = self.get_text_chunks(raw_text)
255
+
256
+ progress(0.6, desc="創建向量存儲...")
257
+ # 創建向量存儲
258
+ success = self.create_vector_store(text_chunks)
259
+
260
+ progress(1.0, desc="處理完成!")
261
+
262
+ if success:
263
+ file_list = "已處理的文件:\n" + "\n".join([f"• {file}" for file in self.processed_files])
264
+ return f"✅ 成功處理 {processed_count} 個PDF文件!\n總共 {len(text_chunks)} 個文字區塊\n現在您可以開始提問。", file_list
265
+ else:
266
+ return "❌ PDF處理失敗,請重試。", ""
267
+
268
+ def clear_data(self):
269
+ """清除處理過的資料"""
270
+ try:
271
+ if os.path.exists("faiss_index"):
272
+ shutil.rmtree("faiss_index")
273
+ self.vector_store = None
274
+ self.processed_files = []
275
+ self.chat_history = []
276
+ return "✅ ��清除所有處理過的資料!", ""
277
+ except Exception as e:
278
+ return f"❌ 清除資料時發生錯誤:{str(e)}", ""
279
+
280
+ def create_docx_report(self, chat_history):
281
+ """創建包含聊天記錄的docx報告"""
282
+ try:
283
+ # 創建新的文檔
284
+ doc = Document()
285
+
286
+ # 添加標題
287
+ title = doc.add_heading('PDF聊天機器人 - 問答記錄', 0)
288
+ title.alignment = 1 # 置中對齊
289
+
290
+ # 添加生成時間
291
+ doc.add_paragraph(f'生成時間:{datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")}')
292
+
293
+ # 添加處理的文件列表
294
+ if self.processed_files:
295
+ doc.add_heading('已處理的PDF文件:', level=2)
296
+ for i, file in enumerate(self.processed_files, 1):
297
+ doc.add_paragraph(f'{i}. {file}', style='List Number')
298
+
299
+ doc.add_paragraph('') # 空行
300
+
301
+ # 添加問答記錄
302
+ doc.add_heading('問答記錄:', level=2)
303
+
304
+ if not chat_history:
305
+ doc.add_paragraph('目前沒有問答記錄。')
306
+ else:
307
+ for i in range(0, len(chat_history), 2):
308
+ if i + 1 < len(chat_history):
309
+ question = chat_history[i]['content']
310
+ answer = chat_history[i + 1]['content']
311
+
312
+ # 問題
313
+ q_paragraph = doc.add_paragraph()
314
+ q_run = q_paragraph.add_run(f'問題 {(i//2)+1}:')
315
+ q_run.bold = True
316
+ q_run.font.size = Inches(0.14)
317
+ q_paragraph.add_run(question)
318
+
319
+ # 回答
320
+ a_paragraph = doc.add_paragraph()
321
+ a_run = a_paragraph.add_run('回答:')
322
+ a_run.bold = True
323
+ a_run.font.size = Inches(0.14)
324
+ a_paragraph.add_run(answer)
325
+
326
+ # 分隔線
327
+ doc.add_paragraph('─' * 50)
328
+
329
+ # 保存到臨時文件
330
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.docx')
331
+ doc.save(temp_file.name)
332
+ temp_file.close()
333
+
334
+ return temp_file.name
335
+
336
+ except Exception as e:
337
+ print(f"創建docx文件時發生錯誤:{str(e)}")
338
+ return None
339
+
340
+ # 初始化聊天機器人
341
+ bot = PDFChatBot()
342
+
343
+ # Gradio 接口函數
344
+ def upload_and_process(files, use_gemini=False, progress=gr.Progress()):
345
+ return bot.process_pdfs(files, progress, use_gemini)
346
+
347
+ def ask_question(question, history, temperature, max_tokens, search_k):
348
+ if not question.strip():
349
+ return history, ""
350
+
351
+ response = bot.answer_question(question, temperature, max_tokens, search_k)
352
+ # 使用新的消息格式
353
+ user_msg = {"role": "user", "content": question}
354
+ assistant_msg = {"role": "assistant", "content": response}
355
+
356
+ history.append(user_msg)
357
+ history.append(assistant_msg)
358
+
359
+ # 同步更新聊天歷史到bot實例
360
+ bot.chat_history = history.copy()
361
+
362
+ return history, ""
363
+
364
+ def download_chat_history():
365
+ """下載聊天記錄為docx文件"""
366
+ if not bot.chat_history:
367
+ return None
368
+
369
+ docx_path = bot.create_docx_report(bot.chat_history)
370
+ return docx_path
371
+
372
+ def export_to_word():
373
+ """匯出問答記錄為Word文件"""
374
+ if not bot.chat_history:
375
+ return None
376
+
377
+ docx_path = bot.create_docx_report(bot.chat_history)
378
+ return docx_path
379
+
380
+ def clear_chat():
381
+ """清除聊天記錄"""
382
+ bot.chat_history = []
383
+ return [], ""
384
+
385
+ def clear_all_data():
386
+ return bot.clear_data()
387
+
388
+ def load_existing_data():
389
+ if bot.load_vector_store():
390
+ return "✅ 成功載入已處理的資料!", ""
391
+ else:
392
+ return "❌ 沒有找到已處理的資料。", ""
393
+
394
+ def set_api_key(api_key: str):
395
+ """設定/更新 Google Gemini API 金鑰。
396
+ 僅在記憶體與環境變數中更新,不會寫入硬碟。"""
397
+ key = (api_key or "").strip()
398
+ if not key:
399
+ return "❌ 未輸入任何金鑰。請貼上有效的 GOOGLE_API_KEY。"
400
+ os.environ["GOOGLE_API_KEY"] = key
401
+ # 重置 embeddings,確保後續以新金鑰初始化
402
+ try:
403
+ bot.embeddings = None
404
+ except Exception:
405
+ pass
406
+ return "✅ 已設定 API 金鑰(僅本次執行期間有效)。"
407
+
408
+ # 創建自定義主題
409
+ custom_theme = gr.themes.Soft(
410
+ primary_hue="blue",
411
+ secondary_hue="gray",
412
+ neutral_hue="slate",
413
+ font=gr.themes.GoogleFont("Noto Sans TC"),
414
+ font_mono=gr.themes.GoogleFont("JetBrains Mono")
415
+ )
416
+
417
+ # 創建 Gradio 介面
418
+ with gr.Blocks(
419
+ title="PDF智能問答系統",
420
+ theme=custom_theme,
421
+ css="""
422
+ .gradio-container {
423
+ max-width: 1200px !important;
424
+ margin: auto !important;
425
+ }
426
+ .main-header {
427
+ text-align: center;
428
+ padding: 20px;
429
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
430
+ color: white;
431
+ border-radius: 10px;
432
+ margin-bottom: 20px;
433
+ }
434
+ .status-box {
435
+ background-color: #f8f9fa;
436
+ border-left: 4px solid #007bff;
437
+ padding: 15px;
438
+ border-radius: 5px;
439
+ }
440
+ .file-info {
441
+ background-color: #e8f5e8;
442
+ border-left: 4px solid #28a745;
443
+ padding: 10px;
444
+ border-radius: 5px;
445
+ }
446
+ """
447
+ ) as demo:
448
+
449
+ # 主標題區域
450
+ with gr.Row():
451
+ gr.HTML("""
452
+ <div class="main-header">
453
+ <h1>🤖 PDF智能問答系統</h1>
454
+ <p>基於 Gemini 2.0 Flash 的 RAG 技術 | 支持多語言問答</p>
455
+ </div>
456
+ """)
457
+
458
+ # 主要功能區域
459
+ with gr.Tab("📁 文件管理", id="file_tab"):
460
+ with gr.Row():
461
+ with gr.Column(scale=3):
462
+ # 文件上傳區域
463
+ with gr.Group():
464
+ gr.Markdown("### 📤 上傳PDF文件")
465
+ api_key_box = gr.Textbox(
466
+ label="Google API Key (可選:部署後可在此貼上)",
467
+ placeholder="以 sk- 或 AIza 開頭的金鑰(不會儲存到硬碟)",
468
+ type="password"
469
+ )
470
+ set_key_btn = gr.Button("🔑 設定 API 金鑰")
471
+ file_upload = gr.File(
472
+ file_count="multiple",
473
+ file_types=[".pdf"],
474
+ label="選擇PDF文件",
475
+ height=150
476
+ )
477
+ use_gemini_toggle = gr.Checkbox(label="使用 Gemini 解析 PDF(支援掃描影像)", value=False)
478
+
479
+ # 處理選項
480
+ with gr.Row():
481
+ process_btn = gr.Button(
482
+ "🚀 開始處理",
483
+ variant="primary",
484
+ size="lg",
485
+ scale=2
486
+ )
487
+ load_btn = gr.Button(
488
+ "📂 載入已處理資料",
489
+ variant="secondary",
490
+ scale=1
491
+ )
492
+ clear_btn = gr.Button(
493
+ "🗑️ 清除所有資料",
494
+ variant="stop",
495
+ scale=1
496
+ )
497
+
498
+ with gr.Column(scale=2):
499
+ # 狀態顯示區域
500
+ with gr.Group():
501
+ gr.Markdown("### 📊 處理狀態")
502
+ status_text = gr.Textbox(
503
+ label="處理進度",
504
+ lines=6,
505
+ interactive=False,
506
+ elem_classes=["status-box"]
507
+ )
508
+
509
+ # 文件列表
510
+ gr.Markdown("### 📋 已處理文件")
511
+ file_list = gr.Textbox(
512
+ label="文件清單",
513
+ lines=8,
514
+ interactive=False,
515
+ elem_classes=["file-info"]
516
+ )
517
+
518
+ with gr.Tab("💬 智能問答", id="chat_tab"):
519
+ with gr.Row():
520
+ with gr.Column(scale=4):
521
+ # 聊天區域
522
+ chatbot = gr.Chatbot(
523
+ label="💬 對話記錄",
524
+ height=600,
525
+ show_copy_button=True,
526
+ type="messages",
527
+ avatar_images=["👤", "🤖"]
528
+ )
529
+
530
+ with gr.Column(scale=1):
531
+ # 側邊欄功能
532
+ with gr.Group():
533
+ gr.Markdown("### ⚙️ 問答設定")
534
+
535
+ # 模型參數調整
536
+ temperature = gr.Slider(
537
+ minimum=0.1,
538
+ maximum=1.0,
539
+ value=0.3,
540
+ step=0.1,
541
+ label="創意度 (Temperature)",
542
+ info="數值越高回答越有創意"
543
+ )
544
+
545
+ max_tokens = gr.Slider(
546
+ minimum=512,
547
+ maximum=8192,
548
+ value=4096,
549
+ step=512,
550
+ label="最大回答長度",
551
+ info="控制回答的詳細程度"
552
+ )
553
+
554
+ search_k = gr.Slider(
555
+ minimum=2,
556
+ maximum=10,
557
+ value=6,
558
+ step=1,
559
+ label="檢索文檔數量",
560
+ info="搜索相關文檔的數量"
561
+ )
562
+
563
+ # 輸入區域
564
+ with gr.Row():
565
+ question_input = gr.Textbox(
566
+ placeholder="請輸入您的問題... (支援中文、英文等多語言)",
567
+ label="💭 問題輸入",
568
+ lines=3,
569
+ scale=4,
570
+ max_lines=5
571
+ )
572
+ ask_btn = gr.Button(
573
+ "📤 發送問題",
574
+ variant="primary",
575
+ scale=1,
576
+ size="lg"
577
+ )
578
+
579
+ # 快捷操作
580
+ with gr.Row():
581
+ clear_chat_btn = gr.Button(
582
+ "🧹 清除對話",
583
+ variant="secondary",
584
+ scale=1
585
+ )
586
+ download_btn = gr.Button(
587
+ "📥 下載問答記錄",
588
+ variant="primary",
589
+ scale=1
590
+ )
591
+ export_btn = gr.Button(
592
+ "📄 匯出為Word",
593
+ variant="secondary",
594
+ scale=1
595
+ )
596
+
597
+ # 問題範例
598
+ with gr.Group():
599
+ gr.Markdown("### 💡 問題範例")
600
+ gr.Examples(
601
+ examples=[
602
+ "這份文檔的主要內容是什麼?",
603
+ "請總結文檔的重點和關鍵概念",
604
+ "文檔中提到了哪些重要數據或統計?",
605
+ "能否詳細解釋某個特定主題或概念?",
606
+ "文檔的結論是什麼?",
607
+ "有哪些重要的建議或建議?",
608
+ "文檔中提到了哪些風險或挑戰?",
609
+ "請比較文檔中提到的不同觀點"
610
+ ],
611
+ inputs=question_input,
612
+ label="點擊範例快速填入"
613
+ )
614
+
615
+ # 隱藏的文件下載組件
616
+ download_file = gr.File(visible=False)
617
+
618
+ # 下載功能處理函數
619
+ def handle_download():
620
+ file_path = download_chat_history()
621
+ if file_path:
622
+ return gr.update(value=file_path, visible=True)
623
+ else:
624
+ gr.Warning("沒有聊天記錄可以下載!")
625
+ return gr.update(visible=False)
626
+
627
+ # 事件處理
628
+ process_btn.click(
629
+ fn=upload_and_process,
630
+ inputs=[file_upload, use_gemini_toggle],
631
+ outputs=[status_text, file_list],
632
+ show_progress=True
633
+ )
634
+
635
+ set_key_btn.click(
636
+ fn=set_api_key,
637
+ inputs=[api_key_box],
638
+ outputs=[status_text]
639
+ )
640
+
641
+ load_btn.click(
642
+ fn=load_existing_data,
643
+ outputs=[status_text, file_list]
644
+ )
645
+
646
+ clear_btn.click(
647
+ fn=clear_all_data,
648
+ outputs=[status_text, file_list]
649
+ )
650
+
651
+ ask_btn.click(
652
+ fn=ask_question,
653
+ inputs=[question_input, chatbot, temperature, max_tokens, search_k],
654
+ outputs=[chatbot, question_input]
655
+ )
656
+
657
+ question_input.submit(
658
+ fn=ask_question,
659
+ inputs=[question_input, chatbot, temperature, max_tokens, search_k],
660
+ outputs=[chatbot, question_input]
661
+ )
662
+
663
+ clear_chat_btn.click(
664
+ fn=clear_chat,
665
+ outputs=[chatbot, question_input]
666
+ )
667
+
668
+ download_btn.click(
669
+ fn=handle_download,
670
+ outputs=download_file
671
+ )
672
+
673
+ export_btn.click(
674
+ fn=export_to_word,
675
+ outputs=download_file
676
+ )
677
+
678
+ if __name__ == "__main__":
679
+ # 嘗試載入現有的向量存儲
680
+ bot.load_vector_store()
681
+
682
+ # 讀取部署相關配置
683
+ server_name = os.getenv("HOST", os.getenv("SERVER_NAME", "0.0.0.0"))
684
+ # 常見平台會傳入 PORT;若無則使用 7860(Gradio 預設)
685
+ server_port_env = os.getenv("PORT", os.getenv("SERVER_PORT"))
686
+ server_port = int(server_port_env) if server_port_env and server_port_env.isdigit() else 7860
687
+ inbrowser = os.getenv("INBROWSER", "false").lower() == "true"
688
+ share = os.getenv("GRADIO_SHARE", "false").lower() == "true"
689
+
690
+ # 啟動應用(綁定 0.0.0.0 以支援容器/雲端)
691
+ demo.launch(
692
+ share=share,
693
+ server_name=server_name,
694
+ server_port=server_port,
695
+ show_error=True,
696
+ inbrowser=inbrowser
697
+ )