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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +2 -697
app.py CHANGED
@@ -1,697 +1,2 @@
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
- )
 
1
+ from G_L_RAG import demo
2
+ app = demo