PhilipL commited on
Commit
3b1c2e5
·
verified ·
1 Parent(s): abf84c0

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +285 -0
  2. mood_food_guide.csv +6 -0
  3. requirements.txt +11 -0
  4. restaurants.csv +0 -0
app.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from openai import OpenAI
3
+ from huggingface_hub import InferenceClient
4
+ import os
5
+ import time
6
+ import requests
7
+ import urllib.parse
8
+ import pandas as pd
9
+ from langchain_huggingface import HuggingFaceEmbeddings
10
+ from langchain_community.vectorstores import FAISS
11
+ from PIL import Image
12
+ from dotenv import load_dotenv
13
+ load_dotenv() # 自動尋找並載入 .env 檔案中的變數
14
+
15
+ # ==========================================
16
+ # 0. 環境變數
17
+ # ==========================================
18
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
19
+ HF_TOKEN = os.getenv("HF_TOKEN")
20
+
21
+ # 全域變數
22
+ global_df = None
23
+ global_mood_df = None
24
+ global_retriever = None
25
+ rag_initialized = False
26
+
27
+ # ==========================================
28
+ # 1. 系統初始化 (讀取 CSV + 讀取 FAISS)
29
+ # ==========================================
30
+ def init_rag_system():
31
+ global global_df, global_mood_df, global_retriever, rag_initialized
32
+
33
+ if rag_initialized: return
34
+
35
+ print("⏳ 正在初始化系統...")
36
+
37
+ # --- A. 讀取 CSV (用於篩選與對照) ---
38
+ try:
39
+ global_df = pd.read_csv('restaurants.csv')
40
+ global_df['RAG_Content'] = global_df['RAG_Content'].fillna("")
41
+ global_df['Category'] = global_df['Category'].fillna("其他")
42
+
43
+ global_mood_df = pd.read_csv('mood_food_guide.csv')
44
+ print(f"✅ CSV 資料讀取成功 (餐廳: {len(global_df)} 筆)")
45
+ except Exception as e:
46
+ print(f"❌ CSV 讀取失敗: {e}")
47
+ return
48
+
49
+ # --- B. 讀取預先建立好的 FAISS 索引 ---
50
+ # 這裡不再重新建立,而是直接讀取 "faiss_index" 資料夾
51
+ if os.path.exists("faiss_index"):
52
+ try:
53
+ print("⏳ 正在載入 FAISS 向量資料庫...")
54
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
55
+
56
+ # allow_dangerous_deserialization=True 是必要的,因為我們讀取 pickle 檔
57
+ # 由於這是我們自己建立的檔案,所以是安全的
58
+ vectorstore = FAISS.load_local(
59
+ "faiss_index",
60
+ embeddings,
61
+ allow_dangerous_deserialization=True
62
+ )
63
+ global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
64
+ print("✅ FAISS 資料庫載入完成!")
65
+ except Exception as e:
66
+ print(f"❌ FAISS 載入失敗: {e}")
67
+ else:
68
+ print("⚠️ 警告:找不到 'faiss_index' 資料夾,RAG 功能將無法使用。")
69
+ print("請先執行 build_index.py 並上傳資料夾。")
70
+
71
+ rag_initialized = True
72
+
73
+ # ==========================================
74
+ # 2. 小測驗與資源設定
75
+ # ==========================================
76
+ custom_css = """
77
+ #hidden_tabs > .tab-nav { display: none !important; visibility: hidden !important; }
78
+ #hidden_tabs > div > button { display: none !important; }
79
+ .vertical-radio fieldset { display: flex !important; flex-direction: column !important; gap: 12px !important; }
80
+ .vertical-radio label { width: 100% !important; margin: 0 !important; display: flex !important; }
81
+ """
82
+
83
+ img_path_1 = "images/image_0.png"
84
+ img_path_2 = "images/image_1.png"
85
+
86
+ if not os.path.exists("images"): os.makedirs("images")
87
+ if not os.path.exists(img_path_1): Image.new('RGB', (400, 300), color='gray').save(img_path_1)
88
+ if not os.path.exists(img_path_2): Image.new('RGB', (400, 300), color='lightblue').save(img_path_2)
89
+
90
+ q1_options_map = {
91
+ "不​要​再​問白癡​問題​了​我​只​想​吃飯​": "現實主義者 (只想吃飯)",
92
+ "去​過隱​居​生活": "隱士 (嚮往平靜)",
93
+ "加入​禁衛軍,​保衛​這個​世界": "守護者 (充滿正義感)",
94
+ "神​羅​天​征,毀滅這個​世界​": "破壞神 (心情可能很差或很中二)"
95
+ }
96
+
97
+ q2_options_map = {
98
+ "不​要​再​問白癡​問題​了​我​只​想​吃飯​": "無魔法 (飢餓度MAX)",
99
+ "麵​包形狀​的​魔法​炸彈": "爆炸魔法 (喜歡刺激/重口味)",
100
+ "毒氣​的​生化​魔法": "毒氣魔法 (可能想吃臭豆腐或特殊風味)",
101
+ "領域​展開:​無量​空處​": "領域展開 (思緒混亂或想放空)"
102
+ }
103
+
104
+ # ==========================================
105
+ # 3. 核心功能
106
+ # ==========================================
107
+
108
+ def get_restaurant_data(mood_score_str, food_choice):
109
+ init_rag_system()
110
+ if global_df is None or global_df.empty: return None, True, "資料庫未載入", "無建議"
111
+
112
+ try:
113
+ score = int(str(mood_score_str).split(' ')[0])
114
+ except:
115
+ score = 3
116
+
117
+ mood_info = global_mood_df[global_mood_df['分數'] == score]
118
+ if not mood_info.empty:
119
+ rec_categories = mood_info.iloc[0]['推薦料理類別']
120
+ mood_reason = mood_info.iloc[0]['原因']
121
+ else:
122
+ rec_categories = ""
123
+ mood_reason = "隨意探索"
124
+
125
+ candidates = global_df.copy()
126
+
127
+ if rec_categories:
128
+ candidates = candidates[candidates['Category'].apply(lambda x: str(x) in str(rec_categories) or str(rec_categories) in str(x))]
129
+
130
+ food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
131
+ if food_keyword:
132
+ candidates = candidates[
133
+ candidates['Name'].str.contains(food_keyword, case=False, na=False) |
134
+ candidates['RAG_Content'].str.contains(food_keyword, case=False, na=False)
135
+ ]
136
+
137
+ if candidates.empty:
138
+ result = global_df.sample(1).iloc[0]
139
+ is_random = True
140
+ else:
141
+ result = candidates.sample(1).iloc[0]
142
+ is_random = False
143
+
144
+ return result, is_random, rec_categories, mood_reason
145
+
146
+ def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, quiz_result, mood_guide_reason):
147
+ if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
148
+
149
+ client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
150
+
151
+ system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友。請根據使用者的日記、心情以及「心情美食指南」來推薦餐廳。"
152
+ user_msg = f"""
153
+ 【狀態】心情分數:{mood_score},日記:{user_diary}
154
+ 【測驗結果】人設:{quiz_result.get('q1', '未知')},魔法:{quiz_result.get('q2', '未知')}
155
+
156
+ 【心情美食指南建議】
157
+ 因為分數是 {mood_score},建議吃這類食物的原因是:「{mood_guide_reason}」。
158
+
159
+ 【推薦餐廳】
160
+ 名稱:{restaurant_name}
161
+ 資料:{restaurant_detail}
162
+
163
+ 任務:
164
+ 請用繁體中文寫一段溫暖有趣的回覆:
165
+ 1. 先回應他的日記與測驗人設。
166
+ 2. 引用「心情美食指南」的原因,告訴他為什麼現在適合吃這家餐廳(例如:「就像指南說的,現在你需要一點多巴胺...」)。
167
+ 3. 介紹這家餐廳的特色。
168
+ (只需要回覆文字內容)
169
+ """
170
+ try:
171
+ response = client.chat.completions.create(
172
+ model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}]
173
+ )
174
+ return response.choices[0].message.content
175
+ except Exception as e:
176
+ return f"Groq Error: {str(e)}"
177
+
178
+ def generate_image_huggingface(prompt):
179
+ if not HF_TOKEN: return None
180
+ if not prompt or pd.isna(prompt): prompt = "Delicious gourmet food, photorealistic, 8k"
181
+
182
+ try:
183
+ hf_client = InferenceClient(token=HF_TOKEN)
184
+ return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
185
+ except Exception as e:
186
+ print(f"❌ 生圖失敗: {e}")
187
+ return None
188
+
189
+ def mood_agent_logic(score_input, food_input, diary_input, quiz_state):
190
+ restaurant, is_random, rec_categories, mood_reason = get_restaurant_data(score_input, food_input)
191
+
192
+ if restaurant is None:
193
+ yield "資料庫讀取錯誤", None, ""
194
+ return
195
+
196
+ name = restaurant['Name']
197
+ address = restaurant['Address']
198
+ url = restaurant['URL']
199
+ img_prompt = restaurant.get('Visual_prompt')
200
+ note = "(隨機推薦)" if is_random else ""
201
+
202
+ # RAG 檢索 (直接用 global_retriever)
203
+ rag_info = str(restaurant.get('RAG_Content', ''))
204
+ if global_retriever:
205
+ docs = global_retriever.invoke(name)
206
+ if docs:
207
+ rag_info = "\n".join([d.page_content for d in docs])
208
+
209
+ ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state, mood_reason)
210
+
211
+ final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}"
212
+ map_html = f'<div style="text-align:center"><a href="{url}" target="_blank" style="background:#4CAF50;color:white;padding:8px 16px;border-radius:20px;text-decoration:none">🗺️ Google Map 導航</a></div>'
213
+
214
+ yield final_response, None, map_html
215
+ image_output = generate_image_huggingface(img_prompt)
216
+ yield final_response, image_output, map_html
217
+
218
+ # ==========================================
219
+ # 4. 介面互動邏輯
220
+ # ==========================================
221
+ # (這部分與之前相同,略)
222
+ def handle_q1_change(selected_label, current_state):
223
+ if not selected_label: return current_state, gr.Button(interactive=False)
224
+ current_state["q1"] = q1_options_map[selected_label]
225
+ return current_state, gr.Button(interactive=True, variant="primary")
226
+
227
+ def handle_q2_change(selected_label, current_state):
228
+ if not selected_label: return current_state, gr.Button(interactive=False)
229
+ current_state["q2"] = q2_options_map[selected_label]
230
+ return current_state, gr.Button(interactive=True, variant="primary", value="完成測驗 (前往點餐) ➔")
231
+
232
+ # ==========================================
233
+ # 5. Gradio 介面建構
234
+ # ==========================================
235
+ with gr.Blocks(title="AI 心情食堂") as demo:
236
+ gr.HTML(f"<style>{custom_css}</style>")
237
+ quiz_state = gr.State(value={})
238
+
239
+ with gr.Tabs(elem_id="hidden_tabs") as tabs:
240
+ # Tab 0: Q1
241
+ with gr.TabItem("Q1", id=0):
242
+ with gr.Column():
243
+ gr.Markdown("### 🔮 第一題:直覺測試")
244
+ gr.Image(value=img_path_1, type="filepath", label="請觀察圖片", height=300)
245
+ gr.Markdown("**問題:請觀察上方圖片,如果是你,你會怎麼做?**")
246
+ radio_q1 = gr.Radio(choices=list(q1_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
247
+ btn_q1_next = gr.Button("下一頁 ➔", interactive=False)
248
+
249
+ # Tab 1: Q2
250
+ with gr.TabItem("Q2", id=1):
251
+ with gr.Column():
252
+ gr.Markdown("### 🔮 第二題:魔法適性")
253
+ gr.Image(value=img_path_2, type="filepath", label="請觀察圖片", height=300)
254
+ gr.Markdown("**問題:身為魔導士的你,會選擇哪一個法術?**")
255
+ radio_q2 = gr.Radio(choices=list(q2_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
256
+ btn_q2_finish = gr.Button("完成測驗 ➔", interactive=False)
257
+
258
+ # Tab 2: Main
259
+ with gr.TabItem("Main", id=2):
260
+ with gr.Column():
261
+ gr.Markdown(f"## 🍱 呷飽沒??!")
262
+ with gr.Row():
263
+ with gr.Column(scale=1):
264
+ score_input = gr.Radio(["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], label="1. 心情分數", value="3 (普通)")
265
+ food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
266
+ diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下今天發生的事...")
267
+ submit_btn = gr.Button("送出給 Agent", variant="primary")
268
+ with gr.Column(scale=1):
269
+ agent_output = gr.Markdown(label="AI 回應")
270
+ image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
271
+ map_output = gr.HTML(label="地圖導航")
272
+
273
+ radio_q1.change(fn=handle_q1_change, inputs=[radio_q1, quiz_state], outputs=[quiz_state, btn_q1_next])
274
+ btn_q1_next.click(fn=lambda: gr.Tabs(selected=1), outputs=tabs)
275
+ radio_q2.change(fn=handle_q2_change, inputs=[radio_q2, quiz_state], outputs=[quiz_state, btn_q2_finish])
276
+ btn_q2_finish.click(fn=lambda: gr.Tabs(selected=2), outputs=tabs)
277
+
278
+ submit_btn.click(
279
+ fn=mood_agent_logic,
280
+ inputs=[score_input, food_input, diary_input, quiz_state],
281
+ outputs=[agent_output, image_output, map_output]
282
+ )
283
+
284
+ if __name__ == "__main__":
285
+ demo.launch(ssr_mode=False)
mood_food_guide.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ 分數,心情狀態,需求,推薦料理類別,原因
2
+ 1,糟透了,"極度需要安慰、溫暖、擁抱感,或者是快速的多巴胺",日式料理(拉麵/壽喜燒/關東煮)、甜點/點心,"當心情跌落谷底,溫暖軟糯的碳水化合物像擁抱;糖分能快速上升血糖帶來短暫快樂,是「人生好難」的解藥。"
3
+ 2,糟糕,"發洩壓力、需要刺激、想要大口吃肉或重口味轉移注意力",韓式料理(炸雞/年糕/部隊鍋)、泰式料理,"辣味帶來的痛覺能刺激腦內啡分泌,酸辣重口能瞬間醒腦,暫時沖刷煩惱。"
4
+ 3,普通,"維持日常機能、追求安穩、健康、不想踩雷也不想太負擔",台式/中式(滷肉飯/水餃/乾麵)、健康餐(水煮/波奇碗),"這是刻在基因裡的安全感味道;或是選擇清淡原型食物,作為一種「自我保養」。"
5
+ 4,不錯,"有些許興致、願意嘗試豐富的香料、適合輕鬆的社交",港式料理、印度料理/馬來西亞料理,"適合花時間慢慢吃的點心,或欣賞香料層次豐富的異國風味,適合心情好且感官敏銳時享用。"
6
+ 5,太棒了,"慶祝、獎勵自己、高熱量、高爽度、大份量分享",美式/西式(牛排/漢堡/披薩/義大利麵),"標準的「慶祝食物」,伴隨著高熱量和飽足感,是大腦認定最高級的「獎勵」。"
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy==1.26.4
2
+ pandas
3
+ langchain
4
+ langchain-community
5
+ langchain-huggingface
6
+ faiss-cpu
7
+ sentence-transformers
8
+ gradio>=4.0.0
9
+ openai
10
+ huggingface_hub
11
+ requests
restaurants.csv ADDED
The diff for this file is too large to render. See raw diff