SiangKai commited on
Commit
48d19d2
·
verified ·
1 Parent(s): 2df3f20

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -35
app.py CHANGED
@@ -18,9 +18,8 @@ import gradio as gr
18
  import pandas as pd
19
  import google.generativeai as genai
20
  from typing import Type
21
-
22
- from collections import defaultdict, OrderedDict
23
  from typing import List, Dict
 
24
 
25
  # LangChain & SentenceTransformers
26
  from langchain_community.vectorstores import FAISS
@@ -116,15 +115,15 @@ def load_data(file_path: str = EXCEL_FILE_PATH) -> pd.DataFrame:
116
 
117
  def batch_find_relevant_tables(api_key: str, sub_queries: list[str], top_k: int = 1) -> dict:
118
  """
119
- (結構化版) 為每個子問題獨立查找候選表,並將完整的配對結構交由 Gemini 判斷。
120
  """
121
- print("🧠 (結構化模式) 正在為每個子問題獨立查找其專屬候選表...")
122
 
123
- # Step 1: 為每個子問題獨立獲取候選表,並存入字典
124
  query_to_candidates_map = {}
125
  for query in sub_queries:
126
  print(f" -> 正在處理: '{query}'")
127
- # 為每個子問題找回 10 個最相關的候選表
128
  candidates_per_query = extract_project_names_from_rag_manual_mix(query, db_jb, db_sim, top_k=20)
129
  # test
130
  print(candidates_per_query)
@@ -140,7 +139,7 @@ def batch_find_relevant_tables(api_key: str, sub_queries: list[str], top_k: int
140
 
141
  # --- Step 2: 動態建構一個新的、結構化的 Prompt ---
142
 
143
- # 建立一個清晰的任務描述文字區塊
144
  tasks_text_parts = []
145
  for i, (query, candidates) in enumerate(query_to_candidates_map.items()):
146
  # 將候選表列表格式化
@@ -161,8 +160,7 @@ def batch_find_relevant_tables(api_key: str, sub_queries: list[str], top_k: int
161
  你是一個專業的數據庫助理。你的任務是從下方的「待處理的配對任務清單」中,根據每一個query,找出最相關的資料表。其餘捨棄。
162
  你必須依[輸出範例]回傳問題及表名,不要有任何多餘的文字、編號、引號或說明。
163
 
164
- 特殊情形:如query為高雄市或各行政區整體(全部))人口數,請一律查詢表名"高雄市戶數、人口密度及性比例"
165
-
166
 
167
  [待處理的配對任務清單]:
168
  {tasks_text}
@@ -177,10 +175,10 @@ def batch_find_relevant_tables(api_key: str, sub_queries: list[str], top_k: int
177
  }}
178
  """.strip()
179
 
180
- # --- Step 3: 呼叫 Gemini 並解析結果---
181
  try:
182
  print("--- Structured Batch Prompt to Gemini ---")
183
- # print(repr(batch_prompt)) # 如果需要偵錯,可以取消註解此行
184
  print("---------------------------------------")
185
 
186
  response_text = reply(api_key, "", batch_prompt)
@@ -195,7 +193,7 @@ def batch_find_relevant_tables(api_key: str, sub_queries: list[str], top_k: int
195
 
196
  def batch_parse_sub_queries_with_gemini(api_key: str, sub_queries: List[str]) -> Dict[str, Dict]:
197
  """
198
- (優化) 一次性批次解析所有子問題,提取時間、地區和查詢項目。
199
  回傳一個以子問題為鍵(key)的字典。
200
  """
201
  print(f"🤖 正在請求 Gemini 批次解析 {len(sub_queries)} 個子問題...")
@@ -214,7 +212,6 @@ def batch_parse_sub_queries_with_gemini(api_key: str, sub_queries: List[str]) ->
214
  1. **時間正規化**:當使用者輸入的時間包含 "年底"、"年中"、"年初"、"年度" 等描述時,請將 `time_query` 正規化為年份。例如,"113年底" 應轉換為 "113年"。
215
  2. **時間校正**:當使用者輸入的時間包含 "年底"、"年中"、"年初" 等描述時,如該問題是有關學校類型(國中小、補習班等概況),請將 `time_query` 修正為學年。例如,"113年" 應轉換為 "113學年"。
216
  3. `district_query` 為可選項目,若無則設為"高雄市全區"。如為"高雄市"或"高雄"等泛指整體者,亦設為"高雄市全區"
217
- 4. 當使用者問題為高雄市或未指定行政區時,item_query: 「總計」+ (time_query轉為西元表示的時間) + <查詢項目文字>
218
  4. 請勿遺漏使用者輸入的任何關鍵詞。
219
 
220
  **情境二:問題模糊,無法查詢**
@@ -230,8 +227,8 @@ def batch_parse_sub_queries_with_gemini(api_key: str, sub_queries: List[str]) ->
230
  ---
231
  [規則]:
232
  1. **時間正規化**:當使用者輸入的時間包含 "年底"、"年中"、"年初"、"年度" 等描述時,請將 `time_query` 正規化為年份。例如,"113年底" 應轉換為 "113年"。
233
- 2. **時間校正**:當使用者輸入的時間包含 "年底"、"年中"、"年初" 等描述時,如該問題是有關教育類型,請將 `time_query` 修正為學年。例如,"113年" 應轉換為 "113學年"。
234
- 3. `district_query` 為可選項目,若無則設為空值。如為"高雄市"亦設為空值。
235
  4. 請勿遺漏使用者輸入的任何關鍵詞。
236
  ---
237
  [輸出格式]:
@@ -267,17 +264,17 @@ def batch_parse_sub_queries_with_gemini(api_key: str, sub_queries: List[str]) ->
267
  # --- 動態查詢工具 ---
268
  def semantic_query_logic(time_query: str, item_query: str, project_name: str, district_query: str = "") -> str:
269
  """
270
- (最終優化版) 直接接收已匹配好的表名,專注於 RAG 檢索排序。
271
  """
272
  print(f"--- 執行查詢: 表名='{project_name}', 時間='{time_query}', 地區='{district_query}', 項目='{item_query}' ---")
273
  df = load_data()
274
  if df is None: return "[]"
275
 
276
- # 步驟 1: (優化) 先用精確的表名進行篩選,大幅縮小範圍
277
  filtered_df = df[df['表名'] == project_name].copy()
278
 
279
  if filtered_df.empty:
280
- # 如果光是表名就找不到任何資料,直接返回
281
  return "[]"
282
 
283
  # 步驟 2: 在已縮小的範圍內,進行時間和地區的篩選
@@ -294,16 +291,16 @@ def semantic_query_logic(time_query: str, item_query: str, project_name: str, di
294
  if filtered_df.empty: return "[]"
295
 
296
  # 關鍵安全閥
297
- MAX_CANDIDATES = 500
298
  if len(filtered_df) > MAX_CANDIDATES:
299
  print(f"⚠️ 篩選結果超過{MAX_CANDIDATES}筆({len(filtered_df)}),僅取前{MAX_CANDIDATES}筆進行向量分析以節省資源。")
300
  filtered_df = filtered_df.head(MAX_CANDIDATES)
301
 
302
- # 步驟 3: (核心) 對最終篩選出的結果進行向量化與語意比對
303
  print(f"向量化階段:對 {len(filtered_df)} 筆資料進行向量化...")
304
  combined_texts = (filtered_df['表名'].astype(str) + " " + filtered_df['表首資訊'].astype(str) + " " + filtered_df['表側資訊'].astype(str)).tolist()
305
 
306
- # 在送入模型前手動加上前綴
307
  prefixed_passage_texts = [f"passage: {t}" for t in combined_texts]
308
  prefixed_query_text = f"query: {item_query}"
309
 
@@ -319,7 +316,7 @@ def semantic_query_logic(time_query: str, item_query: str, project_name: str, di
319
 
320
  results.sort(key=lambda x: x['語意分數'], reverse=True)
321
 
322
- FINAL_K = 80
323
  top_results = results[:FINAL_K]
324
 
325
  print("--- semantic_query_logic 執行完畢 ---")
@@ -331,7 +328,7 @@ from langchain.tools import StructuredTool
331
  semantic_query_tool = StructuredTool.from_function(
332
  func=semantic_query_logic,
333
  name="semantic_query_tool",
334
- description="(純RAG簡化版) 直接使用向量語意模型進行檢索排序。" # 更新描述
335
  )
336
 
337
  # =======================================================================
@@ -344,7 +341,6 @@ system_reviewer = """
344
  1. 涉及高雄市以外或全國性資料,請直接回傳:「抱歉~我是高雄市查詢機器人,無法查詢高雄以外資料。」
345
  2. 未提及明確時間(如112年、113年3月),請回傳:「抱歉~請問查詢的資料時間。」
346
  📌 明確時間=出現「具體年份」、「年月」、「季」或「學年」。模糊詞(平均、近年、目前、歷年等)皆視為未指定。
347
- 3. 問題中的「高雄市」字樣請略過,例如「113年底高雄市人口」視為「113年總人口」。
348
 
349
  📌 回傳格式(**僅限 JSON 陣列**,不得加上任何文字):
350
  [
@@ -390,9 +386,9 @@ system_integration = """
390
  4. **條列推論**:逐項列出比較結果,明確指出最高、最低、差異。
391
  5. **禁止**:不得使用科學記號、英文、原欄位名稱;不得補資料或推論未查到的年份。
392
  ### 二、一般整合型問題(如「113年底苓雅區人口?」):
393
- 1. **條件驗證**:若資料年份不同,請說明「您問的是 113 年,我找到的是 114 年…」
394
  2. **缺資料處理**:無資料請說「資料缺乏,無法回答」;不���用其他時間資料代替。
395
- 3. **作答格式**:300 字內、結論先行、條列清楚、千分位數字,不使用科學記號。開頭統一:「關於您提出的問題,綜合參考資料如下:」,結尾列出參考資料表名(參考資料:高雄市原住民戶口數)。
396
  ---
397
  ## 📌 共通禁止事項(適用所有問題):
398
  - ❌ 不得推論或補未查到的資料
@@ -409,7 +405,7 @@ system_integration = """
409
 
410
  def reply(api_key: str, system: str, prompt: str, model: str = "gemini-2.0-flash-lite"):
411
  """
412
- (非串流版) 一次性獲取完整的 Gemini 回應。
413
  """
414
  try:
415
  genai.configure(api_key=api_key)
@@ -449,9 +445,8 @@ def extract_json(text: str) -> list | dict:
449
  raise ValueError(f"清理後仍然無法解析 JSON。原始錯誤: {e}")
450
 
451
 
452
- def reflect_post(api_key, user_input):
453
  """
454
- (最終優化版 / Gemini批次解析 / 非串流)
455
  API 呼叫總次數固定為 4 次。
456
  """
457
  # Step 1:拆解子問題 (API Call #1)
@@ -478,7 +473,7 @@ def reflect_post(api_key, user_input):
478
  return all_querys_summary, "⚠️ 系統無法為您的查詢匹配到合適的資料表。"
479
 
480
  # Step 3: 批次解析所有子問題的參數 (API Call #3)
481
- params_map = batch_parse_sub_queries_with_gemini(api_key, sub_query_texts)
482
  if not params_map:
483
  return all_querys_summary, "⚠️ 系統無法解析您問題中的查詢參數。"
484
 
@@ -514,7 +509,7 @@ def reflect_post(api_key, user_input):
514
 
515
  # Step 5:整合分析 (API Call #4)
516
  integration_prompt = f"使用者問題:{user_input}\n\n查詢資料如下:\n{combined_context}"
517
- integration_result = reply(api_key, system_integration, integration_prompt)
518
  return all_querys_summary, integration_result
519
 
520
  # =======================================================================
@@ -522,8 +517,9 @@ def reflect_post(api_key, user_input):
522
  # =======================================================================
523
 
524
  def gradio_interface(user_input):
525
- """Gradio 的主要處理函式"""
526
  api_key = os.getenv('Gemini')
 
527
  if not api_key:
528
  return "❌ 查詢失敗", "錯誤:未在伺服器環境中設定 'Gemini' API 金鑰。"
529
 
@@ -546,6 +542,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange"))
546
  """
547
  # 🤖 高雄市公務統計資料智慧查詢
548
  歡迎使用!您可以透過自然語言提出關於高雄市的公務統計問題,系統將盡力為您查找相關資訊。
 
549
  """
550
  )
551
 
@@ -553,15 +550,16 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="orange"))
553
  with gr.Column(scale=1):
554
  user_input_box = gr.Textbox(
555
  label="請在此輸入您的問題",
556
- placeholder="例如:113年底前金區全區人口數?",
557
  lines=5
558
  )
559
  gr.Examples(
560
  examples=[
561
- "113年底前金區全區人口數?",
562
  "110-113年高雄市總人口數趨勢?",
563
  "110-113年失業率情形",
564
- "國小一年級學生人數(缺少時間不能查)",
 
565
  ],
566
  inputs=user_input_box,
567
  label="💡 範例問題"
 
18
  import pandas as pd
19
  import google.generativeai as genai
20
  from typing import Type
 
 
21
  from typing import List, Dict
22
+ from collections import defaultdict, OrderedDict
23
 
24
  # LangChain & SentenceTransformers
25
  from langchain_community.vectorstores import FAISS
 
115
 
116
  def batch_find_relevant_tables(api_key: str, sub_queries: list[str], top_k: int = 1) -> dict:
117
  """
118
+ 為每個子問題獨立查找候選表,並將完整的配對結構交由 Gemini 判斷。
119
  """
120
+ print("🧠 正在為每個子問題獨立查找其相對應候選表...")
121
 
122
+ # --- Step 1: 為每個子問題獨立獲取候選表,並存入字典 ---
123
  query_to_candidates_map = {}
124
  for query in sub_queries:
125
  print(f" -> 正在處理: '{query}'")
126
+ # 為每個子問題找回 20 個最相關的候選表
127
  candidates_per_query = extract_project_names_from_rag_manual_mix(query, db_jb, db_sim, top_k=20)
128
  # test
129
  print(candidates_per_query)
 
139
 
140
  # --- Step 2: 動態建構一個新的、結構化的 Prompt ---
141
 
142
+ # 建立任務描述文字
143
  tasks_text_parts = []
144
  for i, (query, candidates) in enumerate(query_to_candidates_map.items()):
145
  # 將候選表列表格式化
 
160
  你是一個專業的數據庫助理。你的任務是從下方的「待處理的配對任務清單」中,根據每一個query,找出最相關的資料表。其餘捨棄。
161
  你必須依[輸出範例]回傳問題及表名,不要有任何多餘的文字、編號、引號或說明。
162
 
163
+ 特殊情形:如query為高雄市或各行政區整體(全部)人口數,請一律查詢表名"高雄市戶數、人口密度及性比例"
 
164
 
165
  [待處理的配對任務清單]:
166
  {tasks_text}
 
175
  }}
176
  """.strip()
177
 
178
+ # --- Step 3: 呼叫 Gemini 並解析結果 ---
179
  try:
180
  print("--- Structured Batch Prompt to Gemini ---")
181
+ # print(repr(batch_prompt)) # 如需偵錯,可取消註解
182
  print("---------------------------------------")
183
 
184
  response_text = reply(api_key, "", batch_prompt)
 
193
 
194
  def batch_parse_sub_queries_with_gemini(api_key: str, sub_queries: List[str]) -> Dict[str, Dict]:
195
  """
196
+ 批次解析所有子問題,提取時間、地區及查詢項目。
197
  回傳一個以子問題為鍵(key)的字典。
198
  """
199
  print(f"🤖 正在請求 Gemini 批次解析 {len(sub_queries)} 個子問題...")
 
212
  1. **時間正規化**:當使用者輸入的時間包含 "年底"、"年中"、"年初"、"年度" 等描述時,請將 `time_query` 正規化為年份。例如,"113年底" 應轉換為 "113年"。
213
  2. **時間校正**:當使用者輸入的時間包含 "年底"、"年中"、"年初" 等描述時,如該問題是有關學校類型(國中小、補習班等概況),請將 `time_query` 修正為學年。例如,"113年" 應轉換為 "113學年"。
214
  3. `district_query` 為可選項目,若無則設為"高雄市全區"。如為"高雄市"或"高雄"等泛指整體者,亦設為"高雄市全區"
 
215
  4. 請勿遺漏使用者輸入的任何關鍵詞。
216
 
217
  **情境二:問題模糊,無法查詢**
 
227
  ---
228
  [規則]:
229
  1. **時間正規化**:當使用者輸入的時間包含 "年底"、"年中"、"年初"、"年度" 等描述時,請將 `time_query` 正規化為年份。例如,"113年底" 應轉換為 "113年"。
230
+ 2. **時間校正**:當使用者輸入的時間包含 "年底"、"年中"、"年初" 等描述時,如該問題是有關學校類型(國中小、補習班等概況),請將 `time_query` 修正為學年。例如,"113年" 應轉換為 "113學年"。
231
+ 3. `district_query` 為可選項目,若無則設為"高雄市全區"。如為"高雄市"或"高雄"等泛指整體者,亦設為"高雄市全區"
232
  4. 請勿遺漏使用者輸入的任何關鍵詞。
233
  ---
234
  [輸出格式]:
 
264
  # --- 動態查詢工具 ---
265
  def semantic_query_logic(time_query: str, item_query: str, project_name: str, district_query: str = "") -> str:
266
  """
267
+ 直接以已匹配好的表名、時間及統計指標查詢合併資料。
268
  """
269
  print(f"--- 執行查詢: 表名='{project_name}', 時間='{time_query}', 地區='{district_query}', 項目='{item_query}' ---")
270
  df = load_data()
271
  if df is None: return "[]"
272
 
273
+ # 步驟 1: 先用精確的表名進行篩選,大幅縮小範圍
274
  filtered_df = df[df['表名'] == project_name].copy()
275
 
276
  if filtered_df.empty:
277
+ # 如果表名就找不到任何資料,直接返回
278
  return "[]"
279
 
280
  # 步驟 2: 在已縮小的範圍內,進行時間和地區的篩選
 
291
  if filtered_df.empty: return "[]"
292
 
293
  # 關鍵安全閥
294
+ MAX_CANDIDATES = 300
295
  if len(filtered_df) > MAX_CANDIDATES:
296
  print(f"⚠️ 篩選結果超過{MAX_CANDIDATES}筆({len(filtered_df)}),僅取前{MAX_CANDIDATES}筆進行向量分析以節省資源。")
297
  filtered_df = filtered_df.head(MAX_CANDIDATES)
298
 
299
+ # 步驟 3: 對最終篩選出的結果進行向量化與語意比對
300
  print(f"向量化階段:對 {len(filtered_df)} 筆資料進行向量化...")
301
  combined_texts = (filtered_df['表名'].astype(str) + " " + filtered_df['表首資訊'].astype(str) + " " + filtered_df['表側資訊'].astype(str)).tolist()
302
 
303
+ # 加上前綴
304
  prefixed_passage_texts = [f"passage: {t}" for t in combined_texts]
305
  prefixed_query_text = f"query: {item_query}"
306
 
 
316
 
317
  results.sort(key=lambda x: x['語意分數'], reverse=True)
318
 
319
+ FINAL_K = 50
320
  top_results = results[:FINAL_K]
321
 
322
  print("--- semantic_query_logic 執行完畢 ---")
 
328
  semantic_query_tool = StructuredTool.from_function(
329
  func=semantic_query_logic,
330
  name="semantic_query_tool",
331
+ description="直接使用向量語意模型進行檢索排序。
332
  )
333
 
334
  # =======================================================================
 
341
  1. 涉及高雄市以外或全國性資料,請直接回傳:「抱歉~我是高雄市查詢機器人,無法查詢高雄以外資料。」
342
  2. 未提及明確時間(如112年、113年3月),請回傳:「抱歉~請問查詢的資料時間。」
343
  📌 明確時間=出現「具體年份」、「年月」、「季」或「學年」。模糊詞(平均、近年、目前、歷年等)皆視為未指定。
 
344
 
345
  📌 回傳格式(**僅限 JSON 陣列**,不得加上任何文字):
346
  [
 
386
  4. **條列推論**:逐項列出比較結果,明確指出最高、最低、差異。
387
  5. **禁止**:不得使用科學記號、英文、原欄位名稱;不得補資料或推論未查到的年份。
388
  ### 二、一般整合型問題(如「113年底苓雅區人口?」):
389
+ 1. **條件驗證**:若資料年份不同,請說明「您問的是 113 年,我找到的是 114 年…」。
390
  2. **缺資料處理**:無資料請說「資料缺乏,無法回答」;不���用其他時間資料代替。
391
+ 3. **作答格式**:300 字內、結論先行、條列清楚、千分位數字,不使用科學記號。開頭統一:「關於您提出的問題,綜合參考資料如下:」,結尾列出參考資料表名:「參考資料:高雄市原住民戶口數」。
392
  ---
393
  ## 📌 共通禁止事項(適用所有問題):
394
  - ❌ 不得推論或補未查到的資料
 
405
 
406
  def reply(api_key: str, system: str, prompt: str, model: str = "gemini-2.0-flash-lite"):
407
  """
408
+ 獲取 Gemini 回應。
409
  """
410
  try:
411
  genai.configure(api_key=api_key)
 
445
  raise ValueError(f"清理後仍然無法解析 JSON。原始錯誤: {e}")
446
 
447
 
448
+ def reflect_post(api_key, api_key2, user_input):
449
  """
 
450
  API 呼叫總次數固定為 4 次。
451
  """
452
  # Step 1:拆解子問題 (API Call #1)
 
473
  return all_querys_summary, "⚠️ 系統無法為您的查詢匹配到合適的資料表。"
474
 
475
  # Step 3: 批次解析所有子問題的參數 (API Call #3)
476
+ params_map = batch_parse_sub_queries_with_gemini(api_key2, sub_query_texts)
477
  if not params_map:
478
  return all_querys_summary, "⚠️ 系統無法解析您問題中的查詢參數。"
479
 
 
509
 
510
  # Step 5:整合分析 (API Call #4)
511
  integration_prompt = f"使用者問題:{user_input}\n\n查詢資料如下:\n{combined_context}"
512
+ integration_result = reply(api_key2, system_integration, integration_prompt)
513
  return all_querys_summary, integration_result
514
 
515
  # =======================================================================
 
517
  # =======================================================================
518
 
519
  def gradio_interface(user_input):
520
+ """Gradio 主要處理函式"""
521
  api_key = os.getenv('Gemini')
522
+ api_key2 = os.getenv('Gemini2')
523
  if not api_key:
524
  return "❌ 查詢失敗", "錯誤:未在伺服器環境中設定 'Gemini' API 金鑰。"
525
 
 
542
  """
543
  # 🤖 高雄市公務統計資料智慧查詢
544
  歡迎使用!您可以透過自然語言提出關於高雄市的公務統計問題,系統將盡力為您查找相關資訊。
545
+ 本系統運作於2v cpu & 16 GB RAM 免費資源,系統速度稍慢(產生結果時間依問題複雜度而定,一般為10至15秒)。
546
  """
547
  )
548
 
 
550
  with gr.Column(scale=1):
551
  user_input_box = gr.Textbox(
552
  label="請在此輸入您的問題",
553
+ placeholder="例如:113年底前金區人口數?",
554
  lines=5
555
  )
556
  gr.Examples(
557
  examples=[
558
+ "113年底前金區人口數?",
559
  "110-113年高雄市總人口數趨勢?",
560
  "110-113年失業率情形",
561
+ "113學年國小一年級學生人數",
562
+ "113年底鹽埕區、三民區、前鎮區、林園區及美濃區人口數"
563
  ],
564
  inputs=user_input_box,
565
  label="💡 範例問題"