Angelguava commited on
Commit
930c8ac
·
verified ·
1 Parent(s): 619cfa7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +210 -152
app.py CHANGED
@@ -1,8 +1,11 @@
1
  """
2
- 多策略 RAG 文件問答系統 v2 — ChromaDB + PDF/DOCX 版本(含 Telegram 推送)
3
 
4
  安裝依賴:
5
  pip install gradio groq pypdf python-docx sentence-transformers numpy chromadb scikit-learn requests
 
 
 
6
  """
7
 
8
  from __future__ import annotations
@@ -29,31 +32,39 @@ from sklearn.feature_extraction.text import TfidfVectorizer
29
 
30
 
31
  # ══════════════════════════════════════════════════════════
32
- # Telegram 推送設定
33
  # ══════════════════════════════════════════════════════════
34
- DEFAULT_TELEGRAM_CHAT_ID = "5731474175"
35
- TELEGRAM_MAX_LEN = 4000 # Telegram 訊息長度限制 (4096),留一些緩衝
36
-
37
-
38
- def send_telegram_message(text: str, chat_id: str, token: str) -> dict:
39
- """將文字訊息送到 Telegram。若文字過長會自動分段傳送。"""
40
- if not token:
41
- return {"ok": False, "error": "尚未提供 Bot Token"}
42
- if not chat_id:
43
- return {"ok": False, "error": "尚未提供 Chat ID"}
44
- if not text:
45
- return {"ok": False, "error": "empty text"}
46
-
47
- url = f"https://api.telegram.org/bot{token}/sendMessage"
48
- results = []
49
- for i in range(0, len(text), TELEGRAM_MAX_LEN):
50
- chunk = text[i:i + TELEGRAM_MAX_LEN]
51
- try:
52
- r = requests.post(url, data={"chat_id": chat_id, "text": chunk}, timeout=15)
53
- results.append(r.json())
54
- except Exception as exc:
55
- results.append({"ok": False, "error": f"{type(exc).__name__}: {exc}"})
56
- return results[-1] if results else {"ok": False, "error": "no chunks sent"}
 
 
 
 
 
 
 
 
57
 
58
 
59
  # ══════════════════════════════════════════════════════════
@@ -381,7 +392,7 @@ class MultiStrategyRAG:
381
  if not self.client:
382
  return (
383
  "⚠ 尚未設定 Groq API Key。\n"
384
- "請在左欄「Step 01」輸入您的 Groq API Key 並點擊「套用」後再提問。\n\n"
385
  "(檢索已完成,可在下方「查看檢索到的文本片段」確認結果)",
386
  source_preview,
387
  )
@@ -414,21 +425,16 @@ class MultiStrategyRAG:
414
  # Gradio UI
415
  # ══════════════════════════════════════════════════════════
416
  STRATEGY_INFO = [
417
- ("semantic", "🔍 語意搜尋", "ChromaDB 向量相似度,最通用"),
418
- ("tfidf", "📊 TF-IDF", "字元 n-gram 關鍵詞統計"),
419
- ("hybrid", "混合搜尋", "語意 + TF-IDF 結果合併去重"),
420
- ("rerank", "🎯 重新排序", "LLM 對候選片段二次評分"),
421
- ("multi_query", "🔄 多查詢擴展", "自動生成多角度問題聯合搜尋"),
422
- ("compress", "✂️ 上下文壓縮", "LLM 提取最相關句子精簡上下文"),
423
- ("parent_child", "📂 父子文檔", "小片段定位 → 回傳對應大片段"),
424
- ("hyde", "💡 HyDE", "先生成假設答案再語意搜尋"),
425
  ]
426
 
427
- # 顯示用 label -> 內部 key 對照表
428
- STRATEGY_LABEL_TO_KEY = {label: key for key, label, _ in STRATEGY_INFO}
429
- STRATEGY_CHOICES = [label for _, label, _ in STRATEGY_INFO]
430
- STRATEGY_DESC_HTML = "<br>".join(f"<b>{label}</b> — {desc}" for _, label, desc in STRATEGY_INFO)
431
-
432
  CSS = """
433
  body, .gradio-container { background:#f5f4f1 !important; }
434
 
@@ -446,43 +452,34 @@ body, .gradio-container { background:#f5f4f1 !important; }
446
  .pill { display:inline-block; margin:10px 5px 0 0; padding:3px 10px; border-radius:16px;
447
  font-size:11px; background:#e8f4f0; color:#2d6a4f; border:1px solid rgba(45,106,79,.2); }
448
  .pill-amber { background:#fdf4e3; color:#b87a1a; border-color:rgba(184,122,26,.25); }
 
449
 
450
  /* API Key 區塊 */
451
- #apikey-box {
452
  background: #fffbf2;
453
  border: 1.5px solid #f0c96a;
454
  border-radius: 10px;
455
  padding: 12px 14px;
456
  margin-bottom: 8px;
457
  }
 
458
 
459
- /* Telegram 區塊 */
460
- #telegram-box {
461
- background: #eef6ff;
462
- border: 1.5px solid #8ec4f0;
463
- border-radius: 10px;
464
- padding: 12px 14px;
465
- margin-bottom: 8px;
466
- }
467
-
468
- /* 策略選擇區塊 */
469
- #strategy-box {
470
  background:#fff;
471
  border:1.5px solid #e5e0d8;
472
  border-radius:10px;
473
  padding:10px 12px;
474
- margin-bottom: 8px;
475
- }
476
- #strategy-box .wrap { gap:6px !important; }
477
- #strategy-box label {
478
- border:1.5px solid #e5e0d8 !important;
479
- border-radius:8px !important;
480
- padding:8px 10px !important;
481
- margin:0 !important;
482
- transition: border-color .15s, background .15s;
483
  }
484
- #strategy-box label:hover { border-color:#2d6a4f !important; }
485
- #strategy-desc { font-size:11px; color:#7a6e67; line-height:1.6; margin-top:6px; }
 
 
 
486
 
487
  .sec-label { font-size:11px; letter-spacing:1.5px; text-transform:uppercase;
488
  color:#7a6e67; font-weight:700; margin:16px 0 8px; }
@@ -490,25 +487,51 @@ body, .gradio-container { background:#f5f4f1 !important; }
490
  border-radius:12px !important; padding:16px !important; }
491
  #ask-btn { background:#2d6a4f !important; color:#fff !important; border:0 !important; border-radius:8px !important; }
492
  #apply-key-btn { background:#b87a1a !important; color:#fff !important; border:0 !important; border-radius:8px !important; }
493
- #send-tg-btn { background:#0088cc !important; color:#fff !important; border:0 !important; border-radius:8px !important; }
494
  """
495
 
496
  HEADER_HTML = """
497
  <div id="hdr">
498
- <div class="hdr-eyebrow">Intelligent Document Analysis · v2</div>
499
  <div class="hdr-title">多策略 RAG 文件問答系統</div>
500
- <div class="hdr-sub">支援 PDF / DOCX 上傳,採用 ChromaDB 持久化向量資料庫與 8 種 RAG 檢索策略,並可將結果至 Telegram</div>
501
  <div>
502
  <span class="pill">▸ Groq API</span>
503
  <span class="pill">▸ llama-3.1-8b-instant</span>
504
  <span class="pill pill-amber">▸ ChromaDB</span>
505
  <span class="pill pill-amber">▸ PDF / DOCX</span>
506
  <span class="pill">▸ SentenceTransformers</span>
507
- <span class="pill">▸ Telegram</span>
508
  </div>
509
  </div>
510
  """
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  EXAMPLE_QS = [
513
  ["這份文件的主要內容是什麼?"],
514
  ["文件中提到哪些重要概念或定義?"],
@@ -519,14 +542,17 @@ EXAMPLE_QS = [
519
 
520
 
521
  def create_interface():
522
- env_key = os.getenv("GROQ_API_KEY", "").strip()
 
 
 
 
523
  rag = MultiStrategyRAG(chroma_path="./chroma_db")
524
- if env_key:
525
- rag.set_api_key(env_key)
526
 
527
  current_strategy = {"key": "semantic"}
528
- # 暫存最近一次回答 / 來源片段,供 Telegram 推送使用
529
- last_result = {"answer": "", "source": ""}
530
 
531
  def apply_api_key(api_key: str):
532
  key = (api_key or "").strip()
@@ -536,46 +562,52 @@ def create_interface():
536
  return f"✓ API Key 已套用({masked})"
537
  return "⚠ API Key 已清除,無法呼叫 LLM"
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  def upload_document(file):
540
  if file is None:
541
  return "⚠ 請選擇 PDF 或 DOCX 檔案"
542
  return rag.load_document(file.name)
543
 
544
- def set_strategy(label: str):
545
- key = STRATEGY_LABEL_TO_KEY.get(label, "semantic")
546
  current_strategy["key"] = key
547
- return f"✓ 已選擇策略:{label}"
548
-
549
- def ask(query, top_k):
550
- answer, source = rag.generate_answer(query, current_strategy["key"], int(top_k))
551
- last_result["answer"] = answer or ""
552
- last_result["source"] = source or ""
553
- return answer, source
554
 
555
- def push_to_telegram(include_source: bool, chat_id_input: str, token_input: str):
556
- if not last_result["answer"]:
557
- return "⚠ 尚無可推送的回答,請先提問。"
558
 
559
- token = (token_input or "").strip()
560
- if not token:
561
- return "⚠ 請先輸入 Telegram Bot Token"
562
-
563
- chat_id = (chat_id_input or "").strip() or DEFAULT_TELEGRAM_CHAT_ID
564
-
565
- text = f"📄 文件:{rag.source_name or '未'}\n"
566
- text += f" 策略:{rag.STRATEGY_MAP.get(current_strategy['key'], current_strategy['key'])}\n\n"
567
- text += f"💬 回答\n{last_result['answer']}"
568
-
569
- if include_source:
570
- text += f"\n\n— 檢索片段 —\n{last_result['source']}"
 
 
571
 
572
- result = send_telegram_message(text, chat_id=chat_id, token=token)
573
- if result.get("ok"):
574
- return "✓ 已成功推送至 Telegram"
575
- return f"✗ 推送失敗:{result.get('description') or result.get('error') or result}"
576
 
577
  with gr.Blocks(
578
- title="多策略 RAG 文件問答 v2",
579
  css=CSS,
580
  theme=gr.themes.Base(
581
  primary_hue=gr.themes.colors.green,
@@ -588,37 +620,13 @@ def create_interface():
588
  # ── 左欄 ──────────────────────────────────
589
  with gr.Column(scale=1, min_width=320, elem_classes="card-box"):
590
 
591
- # Step 00 · Telegram 推送(移至最上方,避免找不到)
592
- gr.HTML("<div class='sec-label'>Step 00 · Telegram 推送</div>")
593
- with gr.Group(elem_id="telegram-box"):
594
- tg_token = gr.Textbox(
595
- label="Bot Token",
596
- placeholder="例如:123456789:AAAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
597
- type="password",
598
- lines=1,
599
- )
600
- tg_chat_id = gr.Textbox(
601
- label="Chat ID",
602
- placeholder=f"預設:{DEFAULT_TELEGRAM_CHAT_ID}(留空使用此預設值)",
603
- lines=1,
604
- )
605
- tg_include_source = gr.Checkbox(
606
- label="同時推送檢索片段內容", value=False
607
- )
608
- send_tg_btn = gr.Button(
609
- "📨 推送回答到 Telegram", elem_id="send-tg-btn"
610
- )
611
- tg_status = gr.Textbox(
612
- label="推送狀態", interactive=False, lines=1
613
- )
614
-
615
- # Step 01:Groq API Key
616
- gr.HTML("<div class='sec-label'>Step 01 · Groq API Key</div>")
617
  with gr.Group(elem_id="apikey-box"):
618
  api_key_input = gr.Textbox(
619
  label="",
620
  placeholder="gsk_xxxxxxxxxxxxxxxxxxxxxxxx",
621
- value=env_key,
622
  type="password",
623
  lines=1,
624
  show_label=False,
@@ -627,43 +635,78 @@ def create_interface():
627
  "套用 API Key", size="sm", elem_id="apply-key-btn"
628
  )
629
  api_key_status = gr.Textbox(
630
- value="✓ API Key 已從環境變數載入" if env_key else "⚠ 尚未設定 API Key",
631
  interactive=False,
632
  lines=1,
633
  label="",
634
  show_label=False,
635
  )
636
 
637
- # Step 02上傳文件
638
- gr.HTML("<div class='sec-label'>Step 02 · 上傳文件</div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  file_input = gr.File(label="PDF / DOCX", file_types=[".pdf", ".docx"])
640
  load_btn = gr.Button("↑ 載入文件")
641
  status = gr.Textbox(label="狀態", interactive=False, lines=3)
642
 
643
- # Step 03:RAG 策略(改用 Radio,可正常點選)
644
- gr.HTML("<div class='sec-label'>Step 03 · 選擇 RAG 策略</div>")
645
- with gr.Group(elem_id="strategy-box"):
646
- strategy_radio = gr.Radio(
647
- choices=STRATEGY_CHOICES,
648
- value=STRATEGY_CHOICES[0],
649
- label="",
650
- show_label=False,
651
- )
652
- gr.HTML(f"<div id='strategy-desc'>{STRATEGY_DESC_HTML}</div>")
653
  strategy_status = gr.Textbox(
654
- value=f"✓ 已選擇策略:{STRATEGY_CHOICES[0]}",
655
  interactive=False,
656
  lines=1,
657
  label="目前策略",
658
  )
 
659
 
660
- # Step 04:參數
661
- gr.HTML("<div class='sec-label'>Step 04 · 搜尋參數</div>")
662
  topk = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Top-K 片段數量")
663
 
664
  # ── 右欄:問答 ────────────────────────────
665
  with gr.Column(scale=2, elem_classes="card-box"):
666
- gr.HTML("<div class='sec-label'>Step 05 · 輸入問題</div>")
667
  qin = gr.Textbox(
668
  label="",
669
  placeholder="例如:這份文件的核心論點是什麼?",
@@ -674,6 +717,9 @@ def create_interface():
674
  gr.HTML("<div class='sec-label'>AI 回答</div>")
675
  ans = gr.Textbox(label="", lines=12, interactive=False)
676
 
 
 
 
677
  with gr.Accordion("▸ 查看檢索到的文本片段", open=False):
678
  src = gr.Textbox(label="", lines=10, interactive=False)
679
 
@@ -682,19 +728,31 @@ def create_interface():
682
  # ── 事件綁定 ──────────────────────────────────
683
  apply_key_btn.click(fn=apply_api_key, inputs=[api_key_input], outputs=[api_key_status])
684
  api_key_input.submit(fn=apply_api_key, inputs=[api_key_input], outputs=[api_key_status])
685
- load_btn.click(fn=upload_document, inputs=[file_input], outputs=[status])
686
- strategy_radio.change(fn=set_strategy, inputs=[strategy_radio], outputs=[strategy_status])
687
- ask_btn.click(fn=ask, inputs=[qin, topk], outputs=[ans, src])
688
- qin.submit(fn=ask, inputs=[qin, topk], outputs=[ans, src])
689
- send_tg_btn.click(
690
- fn=push_to_telegram,
691
- inputs=[tg_include_source, tg_chat_id, tg_token],
692
  outputs=[tg_status],
693
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
  return demo
696
 
697
 
698
  if __name__ == "__main__":
699
  demo = create_interface()
700
- demo.launch(share=False, server_name="0.0.0.0")
 
1
  """
2
+ 多策略 RAG 文件問答系統 v2 — ChromaDB + PDF/DOCX + Telegram 推播版
3
 
4
  安裝依賴:
5
  pip install gradio groq pypdf python-docx sentence-transformers numpy chromadb scikit-learn requests
6
+
7
+ 執行:
8
+ python rag_telegram_app.py
9
  """
10
 
11
  from __future__ import annotations
 
32
 
33
 
34
  # ══════════════════════════════════════════════════════════
35
+ # Telegram 推播工具
36
  # ══════════════════════════════════════════════════════════
37
+ TELEGRAM_MAX_LEN = 4000 # Telegram 訊息長度上限為 4096,留一點餘量
38
+
39
+
40
+ def send_telegram_message(token: str, chat_id: str, text: str) -> tuple[bool, str]:
41
+ """將文字訊息發送至指定的 Telegram 聊天室。"""
42
+ token = (token or "").strip()
43
+ chat_id = (chat_id or "").strip()
44
+ if not token or not chat_id:
45
+ return False, "⚠ 尚未設定 Telegram Bot Token 或 Chat ID"
46
+
47
+ if len(text) > TELEGRAM_MAX_LEN:
48
+ text = text[:TELEGRAM_MAX_LEN] + "\n…(內容過長,已截斷)"
49
+
50
+ try:
51
+ resp = requests.post(
52
+ f"https://api.telegram.org/bot{token}/sendMessage",
53
+ data={"chat_id": chat_id, "text": text},
54
+ timeout=10,
55
+ )
56
+ data = resp.json()
57
+ if data.get("ok"):
58
+ return True, " 已成功發送至 Telegram"
59
+ return False, f" Telegram 發送失敗:{data.get('description', '未知錯誤')}"
60
+ except Exception as exc:
61
+ return False, f"✗ Telegram 發送失敗:{type(exc).__name__}: {exc}"
62
+
63
+
64
+ def test_telegram_connection(token: str, chat_id: str) -> str:
65
+ """測試 Telegram Bot Token / Chat ID 是否正確(不會打擾使用者太多)。"""
66
+ ok, msg = send_telegram_message(token, chat_id, "✅ RAG 系統已連接,測試訊息成功!")
67
+ return msg if not ok else "✓ 連線成功,已發送測試訊息"
68
 
69
 
70
  # ══════════════════════════════════════════════════════════
 
392
  if not self.client:
393
  return (
394
  "⚠ 尚未設定 Groq API Key。\n"
395
+ "請在左欄「Step 00」輸入您的 Groq API Key 並點擊「套用」後再提問。\n\n"
396
  "(檢索已完成,可在下方「查看檢索到的文本片段」確認結果)",
397
  source_preview,
398
  )
 
425
  # Gradio UI
426
  # ══════════════════════════════════════════════════════════
427
  STRATEGY_INFO = [
428
+ ("semantic", "語意搜尋", "ChromaDB 向量相似度,最通用", "🔍"),
429
+ ("tfidf", "TF-IDF", "字元 n-gram 關鍵詞統計", "📊"),
430
+ ("hybrid", "混合搜尋", "語意 + TF-IDF 結果合併去重", "⚡"),
431
+ ("rerank", "重新排序", "LLM 對候選片段二次評分", "🎯"),
432
+ ("multi_query", "多查詢擴展", "自動生成多角度問題聯合搜尋", "🔄"),
433
+ ("compress", "上下文壓縮", "LLM 提取最相關句子精簡上下文", "✂️"),
434
+ ("parent_child", "父子文檔", "小片段定位 → 回傳對應大片段", "📂"),
435
+ ("hyde", "HyDE", "先生成假設答案再語意搜尋", "💡"),
436
  ]
437
 
 
 
 
 
 
438
  CSS = """
439
  body, .gradio-container { background:#f5f4f1 !important; }
440
 
 
452
  .pill { display:inline-block; margin:10px 5px 0 0; padding:3px 10px; border-radius:16px;
453
  font-size:11px; background:#e8f4f0; color:#2d6a4f; border:1px solid rgba(45,106,79,.2); }
454
  .pill-amber { background:#fdf4e3; color:#b87a1a; border-color:rgba(184,122,26,.25); }
455
+ .pill-blue { background:#e8f0fd; color:#1a5cb8; border-color:rgba(26,92,184,.25); }
456
 
457
  /* API Key 區塊 */
458
+ #apikey-box, #telegram-box {
459
  background: #fffbf2;
460
  border: 1.5px solid #f0c96a;
461
  border-radius: 10px;
462
  padding: 12px 14px;
463
  margin-bottom: 8px;
464
  }
465
+ #telegram-box { background:#f0f6ff; border-color:#a9c8f0; }
466
 
467
+ .strat-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin:10px 0 16px; }
468
+ .strat-card {
 
 
 
 
 
 
 
 
 
469
  background:#fff;
470
  border:1.5px solid #e5e0d8;
471
  border-radius:10px;
472
  padding:10px 12px;
473
+ cursor:pointer;
474
+ transition:border-color .15s, box-shadow .15s;
475
+ text-align:left;
476
+ width:100%;
 
 
 
 
 
477
  }
478
+ .strat-card:hover { border-color:#2d6a4f; box-shadow:0 2px 8px rgba(45,106,79,.12); }
479
+ .strat-card.active { border-color:#2d6a4f; background:#f0f9f5; box-shadow:0 2px 10px rgba(45,106,79,.18); }
480
+ .strat-icon { font-size:20px; margin-bottom:4px; }
481
+ .strat-name { font-size:13px; font-weight:700; color:#1a1714; margin:0 0 2px; }
482
+ .strat-desc { font-size:11px; color:#7a6e67; line-height:1.4; }
483
 
484
  .sec-label { font-size:11px; letter-spacing:1.5px; text-transform:uppercase;
485
  color:#7a6e67; font-weight:700; margin:16px 0 8px; }
 
487
  border-radius:12px !important; padding:16px !important; }
488
  #ask-btn { background:#2d6a4f !important; color:#fff !important; border:0 !important; border-radius:8px !important; }
489
  #apply-key-btn { background:#b87a1a !important; color:#fff !important; border:0 !important; border-radius:8px !important; }
490
+ #apply-tg-btn, #test-tg-btn { background:#1a5cb8 !important; color:#fff !important; border:0 !important; border-radius:8px !important; }
491
  """
492
 
493
  HEADER_HTML = """
494
  <div id="hdr">
495
+ <div class="hdr-eyebrow">Intelligent Document Analysis · v2 + Telegram</div>
496
  <div class="hdr-title">多策略 RAG 文件問答系統</div>
497
+ <div class="hdr-sub">支援 PDF / DOCX 上傳,採用 ChromaDB 持久化向量資料庫與 8 種 RAG 檢索策略,並可將回答至 Telegram</div>
498
  <div>
499
  <span class="pill">▸ Groq API</span>
500
  <span class="pill">▸ llama-3.1-8b-instant</span>
501
  <span class="pill pill-amber">▸ ChromaDB</span>
502
  <span class="pill pill-amber">▸ PDF / DOCX</span>
503
  <span class="pill">▸ SentenceTransformers</span>
504
+ <span class="pill pill-blue">▸ Telegram Bot</span>
505
  </div>
506
  </div>
507
  """
508
 
509
+
510
+ def build_strategy_menu(selected: str = "semantic") -> str:
511
+ cards = []
512
+ for key, name, desc, icon in STRATEGY_INFO:
513
+ active_cls = "active" if key == selected else ""
514
+ cards.append(
515
+ f"""<button class="strat-card {active_cls}" onclick="selectStrategy('{key}', this)" type="button">
516
+ <div class="strat-icon">{icon}</div>
517
+ <div class="strat-name">{name}</div>
518
+ <div class="strat-desc">{desc}</div>
519
+ </button>"""
520
+ )
521
+ return f'<div class="strat-grid">{"".join(cards)}</div>'
522
+
523
+
524
+ STRATEGY_MENU_JS = """
525
+ <script>
526
+ function selectStrategy(key, el) {
527
+ document.querySelectorAll('.strat-card').forEach(c => c.classList.remove('active'));
528
+ el.classList.add('active');
529
+ const inp = document.getElementById('strategy-hidden');
530
+ if (inp) { inp.value = key; inp.dispatchEvent(new Event('input')); }
531
+ }
532
+ </script>
533
+ """
534
+
535
  EXAMPLE_QS = [
536
  ["這份文件的主要內容是什麼?"],
537
  ["文件中提到哪些重要概念或定義?"],
 
542
 
543
 
544
  def create_interface():
545
+ # 啟動時嘗試從環境變數讀取(建議使用環境變數,避免在介面或程式碼中硬編碼)
546
+ env_groq_key = os.getenv("GROQ_API_KEY", "").strip()
547
+ env_tg_token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
548
+ env_tg_chat_id = os.getenv("TELEGRAM_CHAT_ID", "").strip()
549
+
550
  rag = MultiStrategyRAG(chroma_path="./chroma_db")
551
+ if env_groq_key:
552
+ rag.set_api_key(env_groq_key)
553
 
554
  current_strategy = {"key": "semantic"}
555
+ telegram_config = {"token": env_tg_token, "chat_id": env_tg_chat_id}
 
556
 
557
  def apply_api_key(api_key: str):
558
  key = (api_key or "").strip()
 
562
  return f"✓ API Key 已套用({masked})"
563
  return "⚠ API Key 已清除,無法呼叫 LLM"
564
 
565
+ def apply_telegram_config(token: str, chat_id: str):
566
+ telegram_config["token"] = (token or "").strip()
567
+ telegram_config["chat_id"] = (chat_id or "").strip()
568
+ if telegram_config["token"] and telegram_config["chat_id"]:
569
+ masked = (
570
+ telegram_config["token"][:6] + "****" + telegram_config["token"][-4:]
571
+ if len(telegram_config["token"]) > 10
572
+ else "****"
573
+ )
574
+ return f"✓ Telegram 設定已套用(Token:{masked} · Chat ID:{telegram_config['chat_id']})"
575
+ return "⚠ Telegram Token 或 Chat ID 尚未完整設定"
576
+
577
+ def do_test_telegram():
578
+ return test_telegram_connection(telegram_config["token"], telegram_config["chat_id"])
579
+
580
  def upload_document(file):
581
  if file is None:
582
  return "⚠ 請選擇 PDF 或 DOCX 檔案"
583
  return rag.load_document(file.name)
584
 
585
+ def set_strategy(key: str):
 
586
  current_strategy["key"] = key
587
+ return f"✓ 已選擇策略:{dict((k, n) for k, n, *_ in STRATEGY_INFO).get(key, key)}"
 
 
 
 
 
 
588
 
589
+ def ask(query, top_k, send_to_telegram):
590
+ answer, source_preview = rag.generate_answer(query, current_strategy["key"], int(top_k))
 
591
 
592
+ telegram_status = ""
593
+ if send_to_telegram:
594
+ strategy_label = MultiStrategyRAG.STRATEGY_MAP.get(
595
+ current_strategy["key"], current_strategy["key"]
596
+ )
597
+ tg_text = (
598
+ f"📄 文件:{rag.source_name or '(載入)'}\n"
599
+ f"🧩 策略:{strategy_label}\n"
600
+ f" 問題:{query}\n\n"
601
+ f"💬 回答:\n{answer}"
602
+ )
603
+ ok, telegram_status = send_telegram_message(
604
+ telegram_config["token"], telegram_config["chat_id"], tg_text
605
+ )
606
 
607
+ return answer, source_preview, telegram_status
 
 
 
608
 
609
  with gr.Blocks(
610
+ title="多策略 RAG 文件問答 v2 + Telegram",
611
  css=CSS,
612
  theme=gr.themes.Base(
613
  primary_hue=gr.themes.colors.green,
 
620
  # ── 左欄 ──────────────────────────────────
621
  with gr.Column(scale=1, min_width=320, elem_classes="card-box"):
622
 
623
+ # Step 00:Groq API Key
624
+ gr.HTML("<div class='sec-label'>Step 00 · Groq API Key</div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  with gr.Group(elem_id="apikey-box"):
626
  api_key_input = gr.Textbox(
627
  label="",
628
  placeholder="gsk_xxxxxxxxxxxxxxxxxxxxxxxx",
629
+ value=env_groq_key,
630
  type="password",
631
  lines=1,
632
  show_label=False,
 
635
  "套用 API Key", size="sm", elem_id="apply-key-btn"
636
  )
637
  api_key_status = gr.Textbox(
638
+ value="✓ API Key 已從環境變數載入" if env_groq_key else "⚠ 尚未設定 API Key",
639
  interactive=False,
640
  lines=1,
641
  label="",
642
  show_label=False,
643
  )
644
 
645
+ # Step 00bTelegram 推播設定
646
+ gr.HTML("<div class='sec-label'>Step 00b · Telegram 推播設定</div>")
647
+ with gr.Group(elem_id="telegram-box"):
648
+ tg_token_input = gr.Textbox(
649
+ label="Bot Token",
650
+ placeholder="123456789:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
651
+ value=env_tg_token,
652
+ type="password",
653
+ lines=1,
654
+ )
655
+ tg_chat_id_input = gr.Textbox(
656
+ label="Chat ID",
657
+ placeholder="例如:123456789",
658
+ value=env_tg_chat_id,
659
+ lines=1,
660
+ )
661
+ with gr.Row():
662
+ apply_tg_btn = gr.Button("套用設定", size="sm", elem_id="apply-tg-btn")
663
+ test_tg_btn = gr.Button("發送測試訊息", size="sm", elem_id="test-tg-btn")
664
+ tg_status = gr.Textbox(
665
+ value=(
666
+ "✓ Telegram 設定已從環境變數載入"
667
+ if env_tg_token and env_tg_chat_id
668
+ else "⚠ 尚未設定 Telegram"
669
+ ),
670
+ interactive=False,
671
+ lines=1,
672
+ label="",
673
+ show_label=False,
674
+ )
675
+ send_to_telegram_checkbox = gr.Checkbox(
676
+ label="提問後自動將回答推播至 Telegram",
677
+ value=False,
678
+ )
679
+
680
+ # Step 01:上傳文件
681
+ gr.HTML("<div class='sec-label'>Step 01 · 上傳文件</div>")
682
  file_input = gr.File(label="PDF / DOCX", file_types=[".pdf", ".docx"])
683
  load_btn = gr.Button("↑ 載入文件")
684
  status = gr.Textbox(label="狀態", interactive=False, lines=3)
685
 
686
+ # Step 02:RAG 策略
687
+ gr.HTML("<div class='sec-label'>Step 02 · 選擇 RAG 策略</div>")
688
+ gr.HTML(build_strategy_menu("semantic"))
689
+ strategy_input = gr.Textbox(
690
+ value="semantic",
691
+ elem_id="strategy-hidden",
692
+ label="",
693
+ visible=False,
694
+ )
 
695
  strategy_status = gr.Textbox(
696
+ value="✓ 已選擇策略:語意搜尋",
697
  interactive=False,
698
  lines=1,
699
  label="目前策略",
700
  )
701
+ gr.HTML(STRATEGY_MENU_JS)
702
 
703
+ # Step 03:參數
704
+ gr.HTML("<div class='sec-label'>Step 03 · 搜尋參數</div>")
705
  topk = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Top-K 片段數量")
706
 
707
  # ── 右欄:問答 ────────────────────────────
708
  with gr.Column(scale=2, elem_classes="card-box"):
709
+ gr.HTML("<div class='sec-label'>Step 04 · 輸入問題</div>")
710
  qin = gr.Textbox(
711
  label="",
712
  placeholder="例如:這份文件的核心論點是什麼?",
 
717
  gr.HTML("<div class='sec-label'>AI 回答</div>")
718
  ans = gr.Textbox(label="", lines=12, interactive=False)
719
 
720
+ gr.HTML("<div class='sec-label'>Telegram 推播狀態</div>")
721
+ tg_send_status = gr.Textbox(label="", lines=1, interactive=False)
722
+
723
  with gr.Accordion("▸ 查看檢索到的文本片段", open=False):
724
  src = gr.Textbox(label="", lines=10, interactive=False)
725
 
 
728
  # ── 事件綁定 ──────────────────────────────────
729
  apply_key_btn.click(fn=apply_api_key, inputs=[api_key_input], outputs=[api_key_status])
730
  api_key_input.submit(fn=apply_api_key, inputs=[api_key_input], outputs=[api_key_status])
731
+
732
+ apply_tg_btn.click(
733
+ fn=apply_telegram_config,
734
+ inputs=[tg_token_input, tg_chat_id_input],
 
 
 
735
  outputs=[tg_status],
736
  )
737
+ test_tg_btn.click(fn=do_test_telegram, outputs=[tg_status])
738
+
739
+ load_btn.click(fn=upload_document, inputs=[file_input], outputs=[status])
740
+ strategy_input.change(fn=set_strategy, inputs=[strategy_input], outputs=[strategy_status])
741
+
742
+ ask_btn.click(
743
+ fn=ask,
744
+ inputs=[qin, topk, send_to_telegram_checkbox],
745
+ outputs=[ans, src, tg_send_status],
746
+ )
747
+ qin.submit(
748
+ fn=ask,
749
+ inputs=[qin, topk, send_to_telegram_checkbox],
750
+ outputs=[ans, src, tg_send_status],
751
+ )
752
 
753
  return demo
754
 
755
 
756
  if __name__ == "__main__":
757
  demo = create_interface()
758
+ demo.launch(share=False, server_name="0.0.0.0")