PhilipL commited on
Commit
178eb0a
·
verified ·
1 Parent(s): 8b3504d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +25 -149
app.py CHANGED
@@ -8,7 +8,6 @@ import time
8
  import requests
9
  import urllib.parse
10
  import pandas as pd
11
- import re # 新增 re 模組用於解析地址
12
  from langchain_huggingface import HuggingFaceEmbeddings
13
  from langchain_community.vectorstores import FAISS
14
  from PIL import Image
@@ -93,86 +92,25 @@ def check_api_key_status(name, key):
93
  return f"✅ 已設定 ({masked})"
94
 
95
  # ==========================================
96
- # 2. Google Maps 爬蟲與資料處理功能
97
  # ==========================================
98
- def extract_city(address):
99
- """
100
- 通用版地址解析:支援中文(台/臺)、英文地址、與郵遞區號判斷
101
- """
102
- if not isinstance(address, str) or not address:
103
- return "未知地區"
104
-
105
- # 1. 預處理:轉小寫以利英文比對,將 '臺' 統一轉為 '台' 以利中文比對
106
- addr_norm = address.lower().replace("臺", "台")
107
-
108
- # 定義對照表 (關鍵字 -> 標準縣市名稱)
109
- # 順序很重要:縣市層級要比區層級優先,但這裡我們主要抓大區域
110
- city_map = {
111
- "keelung": "基隆市", "基隆": "基隆市",
112
- "taipei": "台北市", "台北": "台北市",
113
- "new taipei": "新北市", "新北": "新北市",
114
- "taoyuan": "桃園市", "桃園": "桃園市",
115
- "hsinchu city": "新竹市", "新竹市": "新竹市",
116
- "hsinchu county": "新竹縣", "新竹縣": "新竹縣",
117
- "hsinchu": "新竹縣市", # 模糊比對放後面
118
- "miaoli": "苗栗縣", "苗栗": "苗栗縣",
119
- "taichung": "台中市", "台中": "台中市",
120
- "changhua": "彰化縣", "彰化": "彰化縣",
121
- "nantou": "南投縣", "南投": "南投縣",
122
- "yunlin": "雲林縣", "雲林": "雲林縣",
123
- "chiayi city": "嘉義市", "嘉義市": "嘉義市",
124
- "chiayi county": "嘉義縣", "嘉義縣": "嘉義縣",
125
- "chiayi": "嘉義縣市",
126
- "tainan": "台南市", "台南": "台南市",
127
- "kaohsiung": "高雄市", "高雄": "高雄市",
128
- "pingtung": "屏東縣", "屏東": "屏東縣",
129
- "yilan": "宜蘭縣", "宜蘭": "宜蘭縣",
130
- "hualien": "花蓮縣", "花蓮": "花蓮縣",
131
- "taitung": "台東縣", "台東": "台東縣",
132
- "penghu": "澎湖縣", "澎湖": "澎湖縣",
133
- "kinmen": "金門縣", "金門": "金門縣",
134
- "lienchiang": "連江縣", "連江": "連江縣", "matsu": "連江縣"
135
- }
136
-
137
- # 2. 關鍵字比對
138
- for key, std_city in city_map.items():
139
- if key in addr_norm:
140
- return std_city
141
-
142
- # 3. 補救措施:正規表達式抓取 "3碼郵遞區號" (台灣地址常出現)
143
- # 例如:106台北市大安區 -> 106 可能是關鍵
144
- # 這邊簡單實作,若需要更精準可能需要郵遞區號對照表,但通常上述關鍵字已足夠
145
-
146
- return "其他地區"
147
-
148
  def sync_google_maps(url):
149
  global global_df, global_retriever, rag_initialized
150
  clean_url = url.strip() if url else ""
151
-
152
- no_update = gr.update()
153
- empty_df = pd.DataFrame() # 空表格
154
-
155
  if not clean_url or "http" not in clean_url.lower():
156
- # 注意:這裡多回傳一個 empty_df
157
- yield "❌ 請輸入有效的 Google Maps 分享連結。", no_update, empty_df
158
  return
159
 
160
  try:
161
- yield "🚀 [1/4] 啟動瀏覽器...", no_update, empty_df
162
  with sync_playwright() as p:
163
- # 加上 user_agent 模擬正常瀏覽器,減少變成英文版的機率 (雖然我們已經做了英文處理)
164
  browser = p.chromium.launch(headless=True)
165
- context = browser.new_context(
166
- locale='zh-TW', # 嘗試強制指定繁體中文環境
167
- timezone_id='Asia/Taipei'
168
- )
169
- page = context.new_page()
170
-
171
- yield "🌐 [2/4] 連線中...", no_update, empty_df
172
  page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
173
- yield "⏳ [3/4] 等待列表加載 (約 10 秒)...", no_update, empty_df
174
  time.sleep(10)
175
- yield "📄 [4/4] 解析餐廳資訊...", no_update, empty_df
176
 
177
  titles = page.locator('div.fontHeadlineSmall').all_inner_texts()
178
  details = page.locator('div.fontBodyMedium').all_inner_texts()
@@ -181,36 +119,23 @@ def sync_google_maps(url):
181
  for i, name in enumerate(titles):
182
  name = name.strip()
183
  if name:
184
- raw_addr = details[i].strip() if i < len(details) else ""
185
- # 這裡使用新的 extract_city
186
- city = extract_city(raw_addr)
187
-
188
- search_query = f"{name} {raw_addr}"
189
  restaurant_list.append({
190
  "Name": name,
191
- "City": city, # 顯示這欄很重要
192
- "Address": raw_addr, # 顯示原始地址以供除錯
193
  "Category": "未分類",
194
- "RAG_Content": f"餐廳:{name},地區:{city},資訊:{raw_addr}",
195
  "URL": f"https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(search_query)}"
196
  })
197
  browser.close()
198
 
199
  if not restaurant_list:
200
- yield "⚠️ 找不到餐廳。請確認連結格式正確且已公開。", no_update, empty_df
201
  return
202
 
203
  global_df = pd.DataFrame(restaurant_list)
204
 
205
- # 準備下拉選單
206
- unique_cities = sorted(global_df['City'].unique().tolist())
207
- if "其他地區" in unique_cities:
208
- unique_cities.remove("其他地區")
209
- unique_cities.append("其他地區")
210
-
211
- dropdown_choices = ["全區"] + unique_cities
212
-
213
- # RAG 初始化 (維持不變)
214
  try:
215
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
216
  vectorstore = FAISS.from_texts(global_df['RAG_Content'].tolist(), embeddings, metadatas=global_df.to_dict('records'))
@@ -219,23 +144,14 @@ def sync_google_maps(url):
219
  print(f"RAG 初始化警告: {e}")
220
 
221
  rag_initialized = True
222
-
223
- # 簡化顯示用的 DataFrame (只秀出重點欄位給使用者看)
224
- display_df = global_df[['Name', 'City', 'Address']].head(20) # 只顯示前20筆避免太長
225
-
226
- # 回傳:訊息, 下拉選單更新, 表格資料更新
227
- yield f"✅ 同步成功!共 {len(restaurant_list)} 筆。", gr.update(choices=dropdown_choices, value="全區", visible=True), display_df
228
-
229
  except Exception as e:
230
- yield f"❌ 系統錯誤:{str(e)}", no_update, empty_df
231
 
232
  # ==========================================
233
  # 3. 核心功能 (AI Agent)
234
  # ==========================================
235
- def get_restaurant_data(mood_score_str, food_choice, location_filter="全區"):
236
- """
237
- 依據心情、食物類別與「地區」篩選餐廳
238
- """
239
  init_static_data() # 確保心情指南已載入
240
 
241
  if global_df is None or global_df.empty:
@@ -256,15 +172,6 @@ def get_restaurant_data(mood_score_str, food_choice, location_filter="全區"):
256
  mood_reason = mood_info.iloc[0]['原因']
257
 
258
  candidates = global_df.copy()
259
-
260
- # --- 新增:地區篩選邏輯 ---
261
- if location_filter and location_filter != "全區":
262
- candidates = candidates[candidates['City'] == location_filter]
263
- # 如果篩選後沒資料,則退回全部資料並標記 Random (或者回傳空值讓前端顯示警告)
264
- if candidates.empty:
265
- return None, True, "", f"在【{location_filter}】找不到餐廳,請嘗試切換地區。"
266
- # ------------------------
267
-
268
  food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
269
 
270
  is_random = False
@@ -276,11 +183,7 @@ def get_restaurant_data(mood_score_str, food_choice, location_filter="全區"):
276
  is_random = True
277
 
278
  if candidates.empty:
279
- # 如果經過兩層篩選都沒東西,就從 global_df 隨便抓 (但盡量遵守地區)
280
- backup_pool = global_df[global_df['City'] == location_filter] if location_filter != "全區" else global_df
281
- if backup_pool.empty: backup_pool = global_df
282
-
283
- result = backup_pool.sample(1).iloc[0]; is_random = True
284
  else:
285
  result = candidates.sample(1).iloc[0]
286
 
@@ -334,18 +237,14 @@ def generate_image_huggingface(prompt):
334
  # 回傳具體錯誤訊息
335
  return None, str(e)
336
 
337
- def mood_agent_logic(score_input, food_input, location_input, diary_input, debug_mode):
338
  if not rag_initialized:
339
  yield "⚠️ 請先同步地圖清單!", None, "", gr.update()
340
  return
341
 
342
- # 傳入 location_input 進行篩選
343
- restaurant, is_random, rec_categories, mood_reason = get_restaurant_data(score_input, food_input, location_input)
344
-
345
- # 錯誤處��:如果 restaurant 是 None,通常是因為該地區沒餐廳
346
- if restaurant is None:
347
- error_msg = mood_reason # 這裡借用 mood_reason 回傳錯誤訊息
348
- yield f"❌ {error_msg}", None, "", gr.update()
349
  return
350
 
351
  name = restaurant['Name']; url = restaurant['URL']
@@ -357,11 +256,8 @@ def mood_agent_logic(score_input, food_input, location_input, diary_input, debug
357
  ai_text, groq_debug_log = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason, debug_mode)
358
 
359
  prefix = ""
360
- if is_random:
361
- if food_input != "隨便":
362
- prefix = f"> 💡 **溫馨提示**:在 {location_input} 的清單中暫無『{food_input}』,已為您挑選其他類型的美味!\n\n"
363
- else:
364
- prefix = f"> 💡 **探索模式**:在 {location_input} 為您精選了一家餐廳!\n\n"
365
 
366
  # 組合完整的除錯資訊,包含 API Key 狀態與錯誤訊息
367
  full_debug_log = ""
@@ -375,7 +271,6 @@ def mood_agent_logic(score_input, food_input, location_input, diary_input, debug
375
  - **GROQ_API_KEY**: {groq_status}
376
  - **HF_TOKEN**: {hf_status}
377
  - **RAG 狀態**: {"✅ 已初始化" if rag_initialized else "❌ 未初始化"}
378
- - **目前篩選地區**: {location_input}
379
  """
380
  full_debug_log = api_debug_block + "\n" + groq_debug_log + f"\n\n### 🎨 Image Prompt Debug\n{img_prompt}"
381
 
@@ -474,19 +369,6 @@ with gr.Blocks(title="AI 心情食堂", css=css_) as demo:
474
  with gr.Row():
475
  map_url = gr.Textbox(label="Google Maps Saved Lists 連結", placeholder="請貼上清單的分享連結...", scale=3)
476
  sync_btn = gr.Button("🔄 同步清單", variant="secondary", scale=1)
477
-
478
- # [NEW] 新增:資料預覽表格
479
- # 放在下拉選單上面或下面都可以,這裡放在下面當作檢查用
480
- with gr.Accordion("📋 點此查看已抓取的餐廳資料 (除錯用)", open=False):
481
- data_preview = gr.Dataframe(
482
- headers=["Name", "City", "Address"],
483
- datatype=["str", "str", "str"],
484
- label="餐廳資料預覽 (顯示前 20 筆)"
485
- )
486
-
487
- # 新增:動態下拉選單
488
- location_dropdown = gr.Dropdown(label="📍 選擇餐廳地區 (同步後自動更新)", choices=["請先同步清單"], value="請先同步清單", interactive=True)
489
-
490
  sync_msg = gr.Markdown("ℹ️ 尚未同步資料庫")
491
  gr.Markdown("---")
492
  gr.Markdown(f"## 🍱 今天想吃點什麼?")
@@ -506,22 +388,16 @@ with gr.Blocks(title="AI 心情食堂", css=css_) as demo:
506
  map_output = gr.HTML(label="地圖導航")
507
 
508
  # 事件
509
- # 修改:sync_btn 現在會同時更新 sync_msg 和 location_dropdown
510
- sync_btn.click(
511
- fn=sync_google_maps,
512
- inputs=[map_url],
513
- outputs=[sync_msg, location_dropdown, data_preview] # 這裡加上 data_preview
514
- )
515
-
516
  btn_start.click(fn=bridge_start_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state, btn_go_dining])
517
  btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
518
  cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, btn_go_dining])
519
  btn_go_dining.click(fn=lambda: gr.Tabs(selected=1), inputs=None, outputs=tabs)
520
 
521
- # 修改:submit_btn 加入 location_dropdown 作為輸入
522
  submit_btn.click(
523
  fn=mood_agent_logic,
524
- inputs=[score_input, food_input, location_dropdown, diary_input, debug_mode_btn],
525
  outputs=[agent_output, image_output, map_output, debug_output]
526
  )
527
 
 
8
  import requests
9
  import urllib.parse
10
  import pandas as pd
 
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
  from PIL import Image
 
92
  return f"✅ 已設定 ({masked})"
93
 
94
  # ==========================================
95
+ # 2. Google Maps 爬蟲功能
96
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  def sync_google_maps(url):
98
  global global_df, global_retriever, rag_initialized
99
  clean_url = url.strip() if url else ""
 
 
 
 
100
  if not clean_url or "http" not in clean_url.lower():
101
+ yield "❌ 請輸入有效的 Google Maps 分享連結。"
 
102
  return
103
 
104
  try:
105
+ yield "🚀 [1/4] 啟動瀏覽器..."
106
  with sync_playwright() as p:
 
107
  browser = p.chromium.launch(headless=True)
108
+ page = browser.new_page()
109
+ yield "🌐 [2/4] 連線中..."
 
 
 
 
 
110
  page.goto(clean_url, wait_until="domcontentloaded", timeout=60000)
111
+ yield "⏳ [3/4] 等待列表加載 (約 10 秒)..."
112
  time.sleep(10)
113
+ yield "📄 [4/4] 解析餐廳資訊..."
114
 
115
  titles = page.locator('div.fontHeadlineSmall').all_inner_texts()
116
  details = page.locator('div.fontBodyMedium').all_inner_texts()
 
119
  for i, name in enumerate(titles):
120
  name = name.strip()
121
  if name:
122
+ addr = details[i].strip() if i < len(details) else ""
123
+ search_query = f"{name} {addr}"
 
 
 
124
  restaurant_list.append({
125
  "Name": name,
126
+ "Address": addr,
 
127
  "Category": "未分類",
128
+ "RAG_Content": f"餐廳:{name},資訊:{addr}",
129
  "URL": f"https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(search_query)}"
130
  })
131
  browser.close()
132
 
133
  if not restaurant_list:
134
+ yield "⚠️ 找不到餐廳。請確認連結格式正確且已公開。"
135
  return
136
 
137
  global_df = pd.DataFrame(restaurant_list)
138
 
 
 
 
 
 
 
 
 
 
139
  try:
140
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
141
  vectorstore = FAISS.from_texts(global_df['RAG_Content'].tolist(), embeddings, metadatas=global_df.to_dict('records'))
 
144
  print(f"RAG 初始化警告: {e}")
145
 
146
  rag_initialized = True
147
+ yield f"✅ 同步成功!已載入 {len(restaurant_list)} 間餐廳。"
 
 
 
 
 
 
148
  except Exception as e:
149
+ yield f"❌ 系統錯誤:{str(e)}"
150
 
151
  # ==========================================
152
  # 3. 核心功能 (AI Agent)
153
  # ==========================================
154
+ def get_restaurant_data(mood_score_str, food_choice):
 
 
 
155
  init_static_data() # 確保心情指南已載入
156
 
157
  if global_df is None or global_df.empty:
 
172
  mood_reason = mood_info.iloc[0]['原因']
173
 
174
  candidates = global_df.copy()
 
 
 
 
 
 
 
 
 
175
  food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
176
 
177
  is_random = False
 
183
  is_random = True
184
 
185
  if candidates.empty:
186
+ result = global_df.sample(1).iloc[0]; is_random = True
 
 
 
 
187
  else:
188
  result = candidates.sample(1).iloc[0]
189
 
 
237
  # 回傳具體錯誤訊息
238
  return None, str(e)
239
 
240
+ def mood_agent_logic(score_input, food_input, diary_input, debug_mode):
241
  if not rag_initialized:
242
  yield "⚠️ 請先同步地圖清單!", None, "", gr.update()
243
  return
244
 
245
+ restaurant, is_random, rec_categories, mood_reason = get_restaurant_data(score_input, food_input)
246
+ if restaurant is None:
247
+ yield "資料庫讀取錯誤", None, "", gr.update()
 
 
 
 
248
  return
249
 
250
  name = restaurant['Name']; url = restaurant['URL']
 
256
  ai_text, groq_debug_log = generate_content_with_groq(name, rag_info, diary_input, score_input, mood_reason, debug_mode)
257
 
258
  prefix = ""
259
+ if is_random and food_input != "隨便":
260
+ prefix = f"> 💡 **溫馨提示**:清單中暫無『{food_input}』,已從現有名單挑選最適合的店!\n\n"
 
 
 
261
 
262
  # 組合完整的除錯資訊,包含 API Key 狀態與錯誤訊息
263
  full_debug_log = ""
 
271
  - **GROQ_API_KEY**: {groq_status}
272
  - **HF_TOKEN**: {hf_status}
273
  - **RAG 狀態**: {"✅ 已初始化" if rag_initialized else "❌ 未初始化"}
 
274
  """
275
  full_debug_log = api_debug_block + "\n" + groq_debug_log + f"\n\n### 🎨 Image Prompt Debug\n{img_prompt}"
276
 
 
369
  with gr.Row():
370
  map_url = gr.Textbox(label="Google Maps Saved Lists 連結", placeholder="請貼上清單的分享連結...", scale=3)
371
  sync_btn = gr.Button("🔄 同步清單", variant="secondary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  sync_msg = gr.Markdown("ℹ️ 尚未同步資料庫")
373
  gr.Markdown("---")
374
  gr.Markdown(f"## 🍱 今天想吃點什麼?")
 
388
  map_output = gr.HTML(label="地圖導航")
389
 
390
  # 事件
391
+ sync_btn.click(fn=sync_google_maps, inputs=[map_url], outputs=[sync_msg])
 
 
 
 
 
 
392
  btn_start.click(fn=bridge_start_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state, btn_go_dining])
393
  btn_stop.click(fn=bridge_stop_click, inputs=[st_state], outputs=[result_markdown, cam, btn_stop, btn_start, st_state])
394
  cam.stream(fn=bridge_predict_frame, inputs=[cam, st_state], outputs=[cam, result_markdown, st_state, btn_stop, btn_start, score_input, btn_go_dining])
395
  btn_go_dining.click(fn=lambda: gr.Tabs(selected=1), inputs=None, outputs=tabs)
396
 
397
+ # debug_mode
398
  submit_btn.click(
399
  fn=mood_agent_logic,
400
+ inputs=[score_input, food_input, diary_input, debug_mode_btn],
401
  outputs=[agent_output, image_output, map_output, debug_output]
402
  )
403