File size: 17,882 Bytes
3b1c2e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11c346
3b1c2e5
 
 
b11c346
3b1c2e5
b11c346
3b1c2e5
b11c346
 
3b1c2e5
6abbb4d
 
 
b11c346
6abbb4d
3b1c2e5
 
 
 
 
b11c346
3b1c2e5
 
 
 
b11c346
 
 
 
 
 
3b1c2e5
 
 
 
 
6abbb4d
b11c346
 
3b1c2e5
 
 
 
 
 
 
 
 
 
 
 
e0b41e1
b11c346
 
 
 
 
 
3b1c2e5
b11c346
6abbb4d
 
3b1c2e5
b11c346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ed9b1d
 
3b1c2e5
 
 
 
 
 
 
 
 
 
 
 
 
6abbb4d
 
 
 
 
 
 
3b1c2e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b11c346
e0b41e1
3b1c2e5
 
 
 
6abbb4d
3ed9b1d
3b1c2e5
 
6abbb4d
b11c346
 
 
 
 
 
3b1c2e5
 
 
 
 
 
 
 
 
 
b11c346
6abbb4d
 
3b1c2e5
 
e0b41e1
3b1c2e5
 
 
 
e0b41e1
 
b11c346
e0b41e1
 
 
 
 
 
 
 
 
 
3b1c2e5
 
 
 
 
 
b11c346
3b1c2e5
 
 
 
 
 
 
b11c346
e0b41e1
3b1c2e5
b11c346
3b1c2e5
 
 
b11c346
6abbb4d
 
 
3b1c2e5
 
b11c346
 
3b1c2e5
 
6abbb4d
b11c346
 
3b1c2e5
b11c346
e0b41e1
3b1c2e5
b11c346
e0b41e1
 
 
 
b11c346
e0b41e1
 
 
 
3b1c2e5
 
b11c346
3b1c2e5
 
 
 
 
b11c346
3b1c2e5
b11c346
 
 
 
3ed9b1d
b11c346
 
 
 
 
 
 
 
 
 
 
 
 
 
3ed9b1d
b11c346
 
 
 
3b1c2e5
 
 
 
 
 
b11c346
3b1c2e5
 
b11c346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ed9b1d
6abbb4d
3ed9b1d
3b1c2e5
 
 
 
 
 
 
b11c346
 
e0b41e1
b11c346
3b1c2e5
 
 
 
 
 
b11c346
 
 
 
 
 
3ed9b1d
b11c346
 
3ed9b1d
b11c346
 
 
 
 
3b1c2e5
b11c346
3b1c2e5
b11c346
e0b41e1
3b1c2e5
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import gradio as gr
from openai import OpenAI
from huggingface_hub import InferenceClient
import os
import time
import requests
import urllib.parse
import pandas as pd
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from PIL import Image
from dotenv import load_dotenv
load_dotenv()  # 自動尋找並載入 .env 檔案中的變數

# ==========================================
# 0. 環境變數
# ==========================================
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
HF_TOKEN = os.getenv("HF_TOKEN")

# 全域變數
global_df = None
global_mood_df = None
global_retriever = None
rag_initialized = False

# ==========================================
# 1. 系統初始化 (讀取 CSV + 讀取 FAISS)
# ==========================================
def init_rag_system():
    global global_df, global_mood_df, global_retriever, rag_initialized
    
    if rag_initialized: return

    print("⏳ 正在初始化系統...")
    
    # --- A. 讀取 CSV (用於篩選與對照) ---
    try:
        global_df = pd.read_csv('restaurants.csv')
        global_df['RAG_Content'] = global_df['RAG_Content'].fillna("")
        global_df['Category'] = global_df['Category'].fillna("其他")
        
        global_mood_df = pd.read_csv('mood_food_guide.csv')
        print(f"✅ CSV 資料讀取成功 (餐廳: {len(global_df)} 筆)")
    except Exception as e:
        print(f"❌ CSV 讀取失敗: {e}")
        return

    # --- B. 讀取預先建立好的 FAISS 索引 ---
    if os.path.exists("faiss_index"):
        try:
            print("⏳ 正在載入 FAISS 向量資料庫...")
            embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
            
            vectorstore = FAISS.load_local(
                "faiss_index", 
                embeddings, 
                allow_dangerous_deserialization=True
            )
            global_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
            print("✅ FAISS 資料庫載入完成!")
        except Exception as e:
            print(f"❌ FAISS 載入失敗: {e}")
    else:
        print("⚠️ 警告:找不到 'faiss_index' 資料夾,RAG 功能將無法使用。")
        print("請先執行 build_index.py 並上傳資料夾。")

    rag_initialized = True

# ==========================================
# 2. 小測驗與資源設定
# ==========================================
custom_css = """
#hidden_tabs > .tab-nav { display: none !important; visibility: hidden !important; }
#hidden_tabs > div > button { display: none !important; }
.vertical-radio fieldset { display: flex !important; flex-direction: column !important; gap: 12px !important; }
.vertical-radio label { width: 100% !important; margin: 0 !important; display: flex !important; }
"""

# 設定圖片路徑
img_path_1 = "images/image_0.png"
img_path_2 = "images/image_1.png"
img_path_3 = "images/image_2.png" # Q3
img_path_4 = "images/image_3.png" # Q4
img_path_5 = "images/image_4.png" # Q5

if not os.path.exists("images"): os.makedirs("images")
for p in [img_path_1, img_path_2, img_path_3, img_path_4, img_path_5]:
    if not os.path.exists(p): 
        Image.new('RGB', (400, 300), color='lightgray').save(p)

# --- 選項設定 ---
q1_options_map = {
    "不​要​再​問白癡​問題​了​我​只​想​吃飯​": "現實主義者 (只想吃飯)",
    "去​過隱​居​生活": "隱士 (嚮往平靜)",
    "加入​禁衛軍,​保衛​這個​世界": "守護者 (充滿正義感)",
    "神​羅​天​征,毀滅這個​世界​": "破壞神 (心情可能很差或很中二)"
}
q2_options_map = {
    "不​要​再​問白癡​問題​了​我​只​想​吃飯​": "無魔法 (飢餓度MAX)",
    "麵​包形狀​的​魔法​炸彈": "爆炸魔法 (喜歡刺激/重口味)",
    "毒氣​的​生化​魔法": "毒氣魔法 (可能想吃臭豆腐或特殊風味)",
    "領域​展開:​無量​空處​": "領域展開 (思緒混亂或想放空)"
}
q3_options_map = {
    "不​要​在​問白癡​問題​了​我​只​想​吃飯​": "現實主義者 (只想吃飯)",
    "歐洲": "嚮往歐洲 (浪漫/西式)",
    "日本": "嚮往日本 (精緻/日式)",
    "泰國": "嚮往泰國 (熱情/酸辣)",
    "轉身搭機捷回家": "戀家 (只想回家)"
}
q4_options_map = {
    "轉頭​回家​睡覺​": "獨行俠 (想睡覺)",
    "看​起來​很​會玩​的​帥潮​": "外向 (找帥潮)",
    "感覺​是​動漫宅​的​同好​": "御宅族 (找同好)",
    "有​興趣​的​異性​": "大膽 (找異性)"
}
q5_options_map = {
    "甚麼​都​不​想​​": "放空 (什麼都不想)",
    "來​自​星星​的​你​": "韓劇迷 (浪漫愛情)",
    "進擊​的​巨人​": "動漫迷 (熱血戰鬥)",
    "洛基": "美劇迷 (懸疑燒腦)"
}

# ==========================================
# 3. 核心功能
# ==========================================

def get_restaurant_data(mood_score_str, food_choice):
    init_rag_system()
    if global_df is None or global_df.empty: return None, True, "資料庫未載入", "無建議"
    
    try:
        score = int(str(mood_score_str).split(' ')[0])
    except:
        score = 3
    
    mood_info = global_mood_df[global_mood_df['分數'] == score]
    if not mood_info.empty:
        rec_categories = mood_info.iloc[0]['推薦料理類別']
        mood_reason = mood_info.iloc[0]['原因']
    else:
        rec_categories = ""
        mood_reason = "隨意探索"

    candidates = global_df.copy()
    
    if rec_categories:
        candidates = candidates[candidates['Category'].apply(lambda x: str(x) in str(rec_categories) or str(rec_categories) in str(x))]
        
    food_keyword = "飯" if food_choice == "吃飯" else "麵" if food_choice == "吃麵" else ""
    if food_keyword:
        candidates = candidates[
            candidates['Name'].str.contains(food_keyword, case=False, na=False) | 
            candidates['RAG_Content'].str.contains(food_keyword, case=False, na=False)
        ]
    
    if candidates.empty:
        result = global_df.sample(1).iloc[0]
        is_random = True
    else:
        result = candidates.sample(1).iloc[0]
        is_random = False
        
    return result, is_random, rec_categories, mood_reason

# [修改] 增加 debug_mode 參數
def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, quiz_result, mood_guide_reason, debug_mode=False):
    if not GROQ_API_KEY: return "⚠️ 請設定 GROQ_API_KEY"
    
    client = OpenAI(api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1")
    
    system_prompt = "你是一個幽默、懂吃且善解人意的 AI 朋友。請根據使用者的日記、心情以及「心情美食指南」來推薦餐廳。"
    
    user_msg = f"""
    【狀態】心情分數:{mood_score},日記:{user_diary}
    
    【五題測驗結果】
    1. 人設直覺:{quiz_result.get('q1', '未知')}
    2. 魔法適性:{quiz_result.get('q2', '未知')}
    3. 旅遊偏好:{quiz_result.get('q3', '未知')}
    4. 社交選擇:{quiz_result.get('q4', '未知')}
    5. 追劇偏好:{quiz_result.get('q5', '未知')}
    
    【心情美食指南建議】
    因為分數是 {mood_score},建議吃這類食物的原因是:「{mood_guide_reason}」。
    
    【推薦餐廳】
    名稱:{restaurant_name}
    資料:{restaurant_detail}
    
    任務:
    請用繁體中文寫一段溫暖有趣的回覆:
    1. 綜合回應他的日記與上述 5 個測驗結果。
    2. 引用「心情美食指南」的原因。
    3. 介紹這家餐廳的特色。
    (只需要回覆文字內容)
    """
    
    try:
        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_msg}]
        )
        content = response.choices[0].message.content
        
        # [修改] 如果開啟除錯模式,附加 Prompt 資訊
        if debug_mode:
            debug_info = f"""
            \n\n--- 🛠️ [DEBUG] LLM Prompt 檢查 ---
            \n**System Prompt:**\n{system_prompt}
            \n**User Message:**\n{user_msg}
            \n-----------------------------------
            """
            return content + debug_info
            
        return content
    except Exception as e:
        return f"Groq Error: {str(e)}"

def generate_image_huggingface(prompt):
    if not HF_TOKEN: return None
    if not prompt or pd.isna(prompt): prompt = "Delicious gourmet food, photorealistic, 8k"
        
    try:
        hf_client = InferenceClient(token=HF_TOKEN)
        return hf_client.text_to_image(prompt=prompt, model="stabilityai/stable-diffusion-xl-base-1.0")
    except Exception as e:
        print(f"❌ 生圖失敗: {e}")
        return None

# [修改] 增加 debug_mode 參數
def mood_agent_logic(score_input, food_input, diary_input, quiz_state, debug_mode):
    restaurant, is_random, rec_categories, mood_reason = get_restaurant_data(score_input, food_input)
    
    if restaurant is None:
        yield "資料庫讀取錯誤", None, ""
        return

    name = restaurant['Name']
    address = restaurant['Address']
    url = restaurant['URL']
    img_prompt = restaurant.get('Visual_prompt') 
    note = "(隨機推薦)" if is_random else ""

    # RAG 檢索
    rag_info = str(restaurant.get('RAG_Content', ''))
    if global_retriever:
        docs = global_retriever.invoke(name)
        if docs:
            rag_info = "\n".join([d.page_content for d in docs])

    # [修改] 傳入 debug_mode
    ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, quiz_state, mood_reason, debug_mode)
    
    # [修改] 如果開啟除錯模式,附加圖片 Prompt 資訊
    if debug_mode:
        img_debug_info = f"""
        \n\n--- 🛠️ [DEBUG] 圖片生成檢查 ---
        \n**使用的 Visual Prompt:**\n{img_prompt}
        \n(如果上方圖片為空白,可能是 HF API 忙碌或 Prompt 無效)
        \n-----------------------------------
        """
        ai_text += img_debug_info

    final_response = f"### 🍽️ 推薦:{name} {note}\n\n{ai_text}"
    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>'
    
    yield final_response, None, map_html
    image_output = generate_image_huggingface(img_prompt)
    yield final_response, image_output, map_html

# ==========================================
# 4. 介面互動邏輯
# ==========================================
def handle_q1_change(selected_label, current_state):
    if not selected_label: return current_state, gr.Button(interactive=False)
    current_state["q1"] = q1_options_map[selected_label]
    return current_state, gr.Button(interactive=True, variant="primary")

def handle_q2_change(selected_label, current_state):
    if not selected_label: return current_state, gr.Button(interactive=False)
    current_state["q2"] = q2_options_map[selected_label]
    return current_state, gr.Button(interactive=True, variant="primary")

def handle_q3_change(selected_label, current_state):
    if not selected_label: return current_state, gr.Button(interactive=False)
    current_state["q3"] = q3_options_map[selected_label]
    return current_state, gr.Button(interactive=True, variant="primary")

def handle_q4_change(selected_label, current_state):
    if not selected_label: return current_state, gr.Button(interactive=False)
    current_state["q4"] = q4_options_map[selected_label]
    return current_state, gr.Button(interactive=True, variant="primary")

def handle_q5_change(selected_label, current_state):
    if not selected_label: return current_state, gr.Button(interactive=False)
    current_state["q5"] = q5_options_map[selected_label]
    return current_state, gr.Button(interactive=True, variant="primary", value="完成測驗 (前往點餐) ➔")

# ==========================================
# 5. Gradio 介面建構
# ==========================================
with gr.Blocks(title="AI 心情食堂") as demo:
    gr.HTML(f"<style>{custom_css}</style>")
    quiz_state = gr.State(value={})

    with gr.Tabs(elem_id="hidden_tabs") as tabs:
        # Tab 0: Q1
        with gr.TabItem("Q1", id=0):
            with gr.Column():
                gr.Markdown("### 🔮 第一題:直覺測試")
                gr.Image(value=img_path_1, type="filepath", label="請觀察圖片", height=300)
                gr.Markdown("**問題:請觀察上方圖片,如果是你,你會怎麼做?**")
                radio_q1 = gr.Radio(choices=list(q1_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
                btn_q1_next = gr.Button("下一頁 ➔", interactive=False)

        # Tab 1: Q2
        with gr.TabItem("Q2", id=1):
            with gr.Column():
                gr.Markdown("### 🔮 第二題:魔法適性")
                gr.Image(value=img_path_2, type="filepath", label="請觀察圖片", height=300)
                gr.Markdown("**問題:身為魔導士的你,會選擇哪一個法術?**")
                radio_q2 = gr.Radio(choices=list(q2_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
                btn_q2_next = gr.Button("下一頁 ➔", interactive=False)

        # Tab 2: Q3
        with gr.TabItem("Q3", id=2):
            with gr.Column():
                gr.Markdown("### 🔮 第三題:旅遊直覺")
                gr.Image(value=img_path_3, type="filepath", label="請觀察圖片", height=300)
                gr.Markdown("**問題三:不考慮其他因素,假你在桃機你最想去哪裡玩?**")
                radio_q3 = gr.Radio(choices=list(q3_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
                btn_q3_next = gr.Button("下一頁 ➔", interactive=False)

        # Tab 3: Q4
        with gr.TabItem("Q4", id=3):
            with gr.Column():
                gr.Markdown("### 🔮 第四題:社交場合")
                gr.Image(value=img_path_4, type="filepath", label="請觀察圖片", height=300)
                gr.Markdown("**問題四:你是小大一,班上還不熟,現在你正參加你們班上的認識彼此的活動,下列哪一個人是你會選擇搭話的人?**")
                radio_q4 = gr.Radio(choices=list(q4_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
                btn_q4_next = gr.Button("下一頁 ➔", interactive=False)

        # Tab 4: Q5
        with gr.TabItem("Q5", id=4):
            with gr.Column():
                gr.Markdown("### 🔮 第五題:追劇時光")
                gr.Image(value=img_path_5, type="filepath", label="請觀察圖片", height=300)
                gr.Markdown("**問題五:假設失憶了忘記以下所列的劇的劇情,而你現在閒了下來剛好想看劇,你會想看哪一部?**")
                radio_q5 = gr.Radio(choices=list(q5_options_map.keys()), label="請選擇", elem_classes="vertical-radio")
                btn_q5_finish = gr.Button("完成測驗 ➔", interactive=False)

        # Tab 5: Main
        with gr.TabItem("Main", id=5):
            with gr.Column():
                gr.Markdown(f"## 🍱 呷飽沒??!")
                with gr.Row():
                    with gr.Column(scale=1):
                        score_input = gr.Radio(["1 (心情差)", "2 (不太好)", "3 (普通)", "4 (不錯)", "5 (超棒)"], label="1. 心情分數", value="3 (普通)")
                        food_input = gr.Radio(["吃飯", "吃麵", "隨便"], label="2. 想吃什麼", value="隨便")
                        diary_input = gr.Textbox(lines=4, label="3. 心情日記", placeholder="寫下今天發生的事...")
                        
                        # [新增] 除錯模式開關
                        debug_mode_btn = gr.Checkbox(label="🔧 開啟除錯模式 (顯示 Prompt 與圖片資訊)", value=False)
                        
                        submit_btn = gr.Button("送出給 Agent", variant="primary")
                    with gr.Column(scale=1):
                        agent_output = gr.Markdown(label="AI 回應")
                        image_output = gr.Image(label="AI 推薦美食圖", type="pil", width=400)
                        map_output = gr.HTML(label="地圖導航")

    # 事件綁定 (Events)
    radio_q1.change(fn=handle_q1_change, inputs=[radio_q1, quiz_state], outputs=[quiz_state, btn_q1_next])
    btn_q1_next.click(fn=lambda: gr.Tabs(selected=1), outputs=tabs)
    
    radio_q2.change(fn=handle_q2_change, inputs=[radio_q2, quiz_state], outputs=[quiz_state, btn_q2_next])
    btn_q2_next.click(fn=lambda: gr.Tabs(selected=2), outputs=tabs)

    radio_q3.change(fn=handle_q3_change, inputs=[radio_q3, quiz_state], outputs=[quiz_state, btn_q3_next])
    btn_q3_next.click(fn=lambda: gr.Tabs(selected=3), outputs=tabs)

    radio_q4.change(fn=handle_q4_change, inputs=[radio_q4, quiz_state], outputs=[quiz_state, btn_q4_next])
    btn_q4_next.click(fn=lambda: gr.Tabs(selected=4), outputs=tabs)

    radio_q5.change(fn=handle_q5_change, inputs=[radio_q5, quiz_state], outputs=[quiz_state, btn_q5_finish])
    btn_q5_finish.click(fn=lambda: gr.Tabs(selected=5), outputs=tabs)

    # [修改] 加入 debug_mode_btn 到輸入
    submit_btn.click(
        fn=mood_agent_logic,
        inputs=[score_input, food_input, diary_input, quiz_state, debug_mode_btn],
        outputs=[agent_output, image_output, map_output]
    )

if __name__ == "__main__":
    demo.launch(ssr_mode=False)