PhilipL commited on
Commit
7c6ecc9
·
verified ·
1 Parent(s): d8fab27

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -58
app.py CHANGED
@@ -11,6 +11,10 @@ from langchain_text_splitters import RecursiveCharacterTextSplitter
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
  from PIL import Image
 
 
 
 
14
 
15
  # ==========================================
16
  # 0. 環境變數與 RAG 初始化
@@ -21,10 +25,18 @@ HF_TOKEN = os.getenv("HF_TOKEN")
21
  # --- 1. 資料讀取 (主要資料庫) ---
22
  df = pd.DataFrame()
23
  try:
24
- df = pd.read_csv('google_maps_rag_data.csv')
 
 
 
 
 
25
  def classify_mood(row):
26
- text = str(row['Name']) + " " + str(row['RAG_Content'])
27
- text = text.lower()
 
 
 
28
  tags = []
29
  rules = {
30
  "開心/慶祝": ["牛排", "steak", "pizza", "炸", "雞排", "甜點", "蛋糕", "cake", "冰", "waffle", "吃到飽", "buffet", "burger", "bistro", "餐酒館"],
@@ -41,33 +53,50 @@ try:
41
 
42
  if 'Mood_Tags' not in df.columns:
43
  df['Mood_Tags'] = df.apply(classify_mood, axis=1)
44
- except:
45
- print("⚠️ 找不到 google_maps_rag_data.csv,RAG 功能將受限")
 
46
 
47
- # --- 2. 資料讀取 (Prompt 資料庫) [新增/修改部分] ---
48
  df_prompts = pd.DataFrame()
 
 
49
  try:
50
- df_prompts = pd.read_csv('restaurants_prompts_v6.csv')
51
- # 確保以 Name 為索引,方便快速查詢
52
- # 假設 CSV 欄位名稱是 'Name' 和 'Image_Prompt',如果不一樣請在這裡改
 
 
 
 
53
  if 'Name' in df_prompts.columns:
54
  df_prompts.set_index('Name', inplace=True)
55
  else:
56
- print("⚠️ Prompt CSV 中找不到 'Name' 欄位")
57
- except:
 
 
 
 
 
58
  print("⚠️ 找不到 restaurants_prompts_v6.csv,將使用預設 Prompt")
 
 
59
 
60
 
61
  # --- RAG 建置 ---
62
  retriever = None
63
- if not df.empty:
64
- loader = DataFrameLoader(df, page_content_column="RAG_Content")
65
- documents = loader.load()
66
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
67
- docs = text_splitter.split_documents(documents)
68
- embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
69
- vectorstore = FAISS.from_documents(docs, embeddings)
70
- retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
 
 
 
71
 
72
  # ==========================================
73
  # 1. 小測驗設定
@@ -92,8 +121,11 @@ img_path_2 = "images/image_1.png"
92
 
93
  # 自動產生替代圖片
94
  if not os.path.exists("images"): os.makedirs("images")
95
- if not os.path.exists(img_path_1): Image.new('RGB', (400, 300), color='gray').save(img_path_1)
96
- if not os.path.exists(img_path_2): Image.new('RGB', (400, 300), color='lightblue').save(img_path_2)
 
 
 
97
 
98
  # 選項對照表
99
  q1_options_map = {
@@ -129,9 +161,15 @@ def get_restaurant_data(mood_score_str, food_choice):
129
  elif food_choice == "吃麵": food_keyword = "麵"
130
 
131
  candidates = df.copy()
 
 
 
 
 
132
  if target_moods:
133
  pattern = "|".join(target_moods)
134
  candidates = candidates[candidates['Mood_Tags'].str.contains(pattern, regex=True, na=False)]
 
135
  if food_keyword:
136
  candidates = candidates[
137
  candidates['Name'].str.contains(food_keyword, case=False, na=False) |
@@ -142,10 +180,7 @@ def get_restaurant_data(mood_score_str, food_choice):
142
  return candidates.sample(1).iloc[0], False
143
 
144
  def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, quiz_result):
145
- """
146
- 修改說明:這裡不再生成 Image Prompt,只專注於生成文字回應。
147
- """
148
- if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
149
 
150
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
151
 
@@ -168,7 +203,7 @@ def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, m
168
  【回應內容】:用繁體中文寫一段溫暖有趣的回覆。
169
  - 結合「日記」與「前測人設」給予回應。
170
  - 推薦上述餐廳,說明為什麼這家店適合他。
171
- - 不需要提供圖片 Prompt,直接回覆內容即可。
172
  """
173
  try:
174
  response = client.chat.completions.create(
@@ -184,29 +219,31 @@ def generate_image_huggingface(prompt):
184
  if not HF_TOKEN: return None
185
  try:
186
  hf_client = InferenceClient(token=HF_TOKEN)
187
- # 您可以使用原本的模型,或嘗試其他模型
188
  return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
189
  except: return None
190
 
191
- # --- 主邏輯 Agent (已修改圖片 Prompt 來源) ---
192
  def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
193
  restaurant, is_random = get_restaurant_data(score_input, food_input)
194
  if restaurant is None:
195
- yield "資料庫讀取錯誤", None, ""
196
  return
197
 
198
- name = restaurant['Name']
199
- address = restaurant['Address']
200
- url = restaurant['URL']
201
  note = "(隨機推薦)" if is_random else ""
202
 
203
  # 1. 處理 RAG 內容
204
- rag_info = str(restaurant['RAG_Content'])
205
  if retriever:
206
- docs = retriever.invoke(name)
207
- if docs: rag_info = "\n".join([d.page_content for d in docs])
 
 
 
208
 
209
- # 2. 呼叫 LLM 產生文字回應 (不再產生 Prompt)
210
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state)
211
 
212
  final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}"
@@ -215,27 +252,30 @@ def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
215
  # 3. 先回傳文字與地圖
216
  yield final_response, None, map_html
217
 
218
- # 4. 查找 CSV 獲取 Prompt 並生成圖片 [修改重點]
219
- img_prompt = f"Delicious food from {name}, cinematic lighting, 8k" # 預設值
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- if not df_prompts.empty and name in df_prompts.index:
222
- try:
223
- # 假設 CSV 中存 Prompt 的欄位叫 'Image_Prompt'��如果不一樣請修改這裡
224
- csv_prompt = df_prompts.loc[name, 'Image_Prompt']
225
- # 如果有多筆資料 (重複名稱),取第一筆
226
- if isinstance(csv_prompt, pd.Series):
227
- csv_prompt = csv_prompt.iloc[0]
228
-
229
- if pd.notna(csv_prompt) and str(csv_prompt).strip() != "":
230
- img_prompt = str(csv_prompt)
231
- print(f"✅ 使用 CSV Prompt ({name}): {img_prompt[:30]}...")
232
- else:
233
- print(f"⚠️ 餐廳 {name} 在 CSV 中,但 Prompt 空白,使用預設值")
234
- except Exception as e:
235
- print(f"⚠️ 讀取 Prompt 發生錯誤: {e}")
236
- else:
237
- print(f"ℹ️ 餐廳 {name} 不在 Prompts CSV 中,使用預設生成規則")
238
-
239
  # 5. 生成圖片
240
  image_output = generate_image_huggingface(img_prompt)
241
  yield final_response, image_output, map_html
@@ -246,12 +286,12 @@ def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
246
 
247
  def handle_q1_change(selected_label, current_state):
248
  if not selected_label: return current_state, gr.Button(interactive=False)
249
- current_state["q1"] = q1_options_map[selected_label]
250
  return current_state, gr.Button(interactive=True, variant="primary")
251
 
252
  def handle_q2_change(selected_label, current_state):
253
  if not selected_label: return current_state, gr.Button(interactive=False)
254
- current_state["q2"] = q2_options_map[selected_label]
255
  return current_state, gr.Button(interactive=True, variant="primary", value="完成測驗 (前往點餐) ➔")
256
 
257
  def go_to_page_2():
 
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
  from langchain_community.vectorstores import FAISS
13
  from PIL import Image
14
+ import warnings
15
+
16
+ # 忽略 pandas 的一些警告
17
+ warnings.filterwarnings("ignore")
18
 
19
  # ==========================================
20
  # 0. 環境變數與 RAG 初始化
 
25
  # --- 1. 資料讀取 (主要資料庫) ---
26
  df = pd.DataFrame()
27
  try:
28
+ # engine='python' 可以避免 Invalid file descriptor 錯誤
29
+ df = pd.read_csv('google_maps_rag_data.csv', engine='python')
30
+
31
+ # 清理欄位名稱 (移除前後空白)
32
+ df.columns = df.columns.str.strip()
33
+
34
  def classify_mood(row):
35
+ # 確保轉成字串以免報錯
36
+ name_str = str(row.get('Name', ''))
37
+ rag_str = str(row.get('RAG_Content', ''))
38
+ text = (name_str + " " + rag_str).lower()
39
+
40
  tags = []
41
  rules = {
42
  "開心/慶祝": ["牛排", "steak", "pizza", "炸", "雞排", "甜點", "蛋糕", "cake", "冰", "waffle", "吃到飽", "buffet", "burger", "bistro", "餐酒館"],
 
53
 
54
  if 'Mood_Tags' not in df.columns:
55
  df['Mood_Tags'] = df.apply(classify_mood, axis=1)
56
+ except Exception as e:
57
+ print(f"⚠️ 讀取 google_maps_rag_data.csv 失敗: {e}")
58
+ print("RAG 功能將受限")
59
 
60
+ # --- 2. 資料讀取 (Prompt 資料庫) [修正重點] ---
61
  df_prompts = pd.DataFrame()
62
+ PROMPT_COL_NAME = 'Image_Prompt' # 設定目標欄位名稱
63
+
64
  try:
65
+ # 使用 engine='python' 避免 file descriptor 錯誤
66
+ df_prompts = pd.read_csv('restaurants_prompts_v6.csv', engine='python')
67
+
68
+ # 自動清除欄位名稱的前後空白 (解決 ' Image_Prompt' 抓不到的問題)
69
+ df_prompts.columns = df_prompts.columns.str.strip()
70
+
71
+ # 檢查並設定 Index
72
  if 'Name' in df_prompts.columns:
73
  df_prompts.set_index('Name', inplace=True)
74
  else:
75
+ print(f"⚠️ Prompts CSV 警告: 找不到 'Name' 欄位,現有欄位: {df_prompts.columns.tolist()}")
76
+
77
+ # 檢查目標欄位是否存在
78
+ if PROMPT_COL_NAME not in df_prompts.columns:
79
+ print(f"⚠️ Prompts CSV 警告: 找不到 '{PROMPT_COL_NAME}' 欄位。現有欄位: {df_prompts.columns.tolist()}")
80
+
81
+ except FileNotFoundError:
82
  print("⚠️ 找不到 restaurants_prompts_v6.csv,將使用預設 Prompt")
83
+ except Exception as e:
84
+ print(f"⚠️ 讀取 Prompt CSV 發生未預期錯誤: {e}")
85
 
86
 
87
  # --- RAG 建置 ---
88
  retriever = None
89
+ if not df.empty and 'RAG_Content' in df.columns:
90
+ try:
91
+ loader = DataFrameLoader(df, page_content_column="RAG_Content")
92
+ documents = loader.load()
93
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
94
+ docs = text_splitter.split_documents(documents)
95
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
96
+ vectorstore = FAISS.from_documents(docs, embeddings)
97
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
98
+ except Exception as e:
99
+ print(f"⚠️ RAG 初始化失敗: {e}")
100
 
101
  # ==========================================
102
  # 1. 小測驗設定
 
121
 
122
  # 自動產生替代圖片
123
  if not os.path.exists("images"): os.makedirs("images")
124
+ try:
125
+ if not os.path.exists(img_path_1): Image.new('RGB', (400, 300), color='gray').save(img_path_1)
126
+ if not os.path.exists(img_path_2): Image.new('RGB', (400, 300), color='lightblue').save(img_path_2)
127
+ except Exception:
128
+ pass # 忽略圖片建立錯誤
129
 
130
  # 選項對照表
131
  q1_options_map = {
 
161
  elif food_choice == "吃麵": food_keyword = "麵"
162
 
163
  candidates = df.copy()
164
+
165
+ # 確保必要欄位存在
166
+ if 'Mood_Tags' not in candidates.columns:
167
+ return df.sample(1).iloc[0], True
168
+
169
  if target_moods:
170
  pattern = "|".join(target_moods)
171
  candidates = candidates[candidates['Mood_Tags'].str.contains(pattern, regex=True, na=False)]
172
+
173
  if food_keyword:
174
  candidates = candidates[
175
  candidates['Name'].str.contains(food_keyword, case=False, na=False) |
 
180
  return candidates.sample(1).iloc[0], False
181
 
182
  def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, quiz_result):
183
+ if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY (系統變數)"
 
 
 
184
 
185
  client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
186
 
 
203
  【回應內容】:用繁體中文寫一段溫暖有趣的回覆。
204
  - 結合「日記」與「前測人設」給予回應。
205
  - 推薦上述餐廳,說明為什麼這家店適合他。
206
+ - 語氣自然,像朋友一樣。
207
  """
208
  try:
209
  response = client.chat.completions.create(
 
219
  if not HF_TOKEN: return None
220
  try:
221
  hf_client = InferenceClient(token=HF_TOKEN)
 
222
  return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
223
  except: return None
224
 
225
+ # --- 主邏輯 Agent ---
226
  def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
227
  restaurant, is_random = get_restaurant_data(score_input, food_input)
228
  if restaurant is None:
229
+ yield "資料庫讀取錯誤或為空", None, ""
230
  return
231
 
232
+ name = str(restaurant['Name'])
233
+ address = str(restaurant.get('Address', '未知地址'))
234
+ url = str(restaurant.get('URL', f'https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(name)}'))
235
  note = "(隨機推薦)" if is_random else ""
236
 
237
  # 1. 處理 RAG 內容
238
+ rag_info = str(restaurant.get('RAG_Content', ''))
239
  if retriever:
240
+ try:
241
+ docs = retriever.invoke(name)
242
+ if docs: rag_info = "\n".join([d.page_content for d in docs])
243
+ except Exception:
244
+ pass
245
 
246
+ # 2. 呼叫 LLM 產生文字回應
247
  ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state)
248
 
249
  final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}"
 
252
  # 3. 先回傳文字與地圖
253
  yield final_response, None, map_html
254
 
255
+ # 4. 查找 CSV 獲取 Prompt 並生成圖片 [安全讀取版]
256
+ img_prompt = f"Delicious food from {name}, cinematic lighting, 8k, photorealistic" # 預設值
257
+
258
+ # 檢查 df_prompts 是否有資料且目標欄位存在
259
+ if not df_prompts.empty and PROMPT_COL_NAME in df_prompts.columns:
260
+ if name in df_prompts.index:
261
+ try:
262
+ csv_prompt = df_prompts.loc[name, PROMPT_COL_NAME]
263
+
264
+ # 處理重複名稱 (如果是 Series 取第一個)
265
+ if isinstance(csv_prompt, pd.Series):
266
+ csv_prompt = csv_prompt.iloc[0]
267
+
268
+ # 檢查是否為 NaN 或空字串
269
+ if pd.notna(csv_prompt) and str(csv_prompt).strip() != "":
270
+ img_prompt = str(csv_prompt)
271
+ # print(f"✅ 使用 CSV Prompt ({name}): {img_prompt[:20]}...")
272
+ else:
273
+ print(f"⚠️ 餐廳 {name} 在 CSV 中,但 Prompt 為空,使用預設值。")
274
+ except Exception as e:
275
+ print(f"⚠️ 取用 Prompt 時發生錯誤: {e}")
276
+ else:
277
+ print(f"ℹ️ 餐廳 '{name}' 不在 Prompts CSV 索引中,使用預設生成規則")
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  # 5. 生成圖片
280
  image_output = generate_image_huggingface(img_prompt)
281
  yield final_response, image_output, map_html
 
286
 
287
  def handle_q1_change(selected_label, current_state):
288
  if not selected_label: return current_state, gr.Button(interactive=False)
289
+ current_state["q1"] = q1_options_map.get(selected_label, selected_label)
290
  return current_state, gr.Button(interactive=True, variant="primary")
291
 
292
  def handle_q2_change(selected_label, current_state):
293
  if not selected_label: return current_state, gr.Button(interactive=False)
294
+ current_state["q2"] = q2_options_map.get(selected_label, selected_label)
295
  return current_state, gr.Button(interactive=True, variant="primary", value="完成測驗 (前往點餐) ➔")
296
 
297
  def go_to_page_2():