Spaces:
Sleeping
Sleeping
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) |