Upload app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def classify_mood(row):
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 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("⚠️
|
|
|
|
| 46 |
|
| 47 |
-
# --- 2. 資料讀取 (Prompt 資料庫) [
|
| 48 |
df_prompts = pd.DataFrame()
|
|
|
|
|
|
|
| 49 |
try:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
if 'Name' in df_prompts.columns:
|
| 54 |
df_prompts.set_index('Name', inplace=True)
|
| 55 |
else:
|
| 56 |
-
print("⚠️
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
print("⚠️ 找不到 restaurants_prompts_v6.csv,將使用預設 Prompt")
|
|
|
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
# --- RAG 建置 ---
|
| 62 |
retriever = None
|
| 63 |
-
if not df.empty:
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 96 |
-
if not os.path.exists(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
-
|
| 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
|
| 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
|
| 200 |
-
url = restaurant
|
| 201 |
note = "(隨機推薦)" if is_random else ""
|
| 202 |
|
| 203 |
# 1. 處理 RAG 內容
|
| 204 |
-
rag_info = str(restaurant
|
| 205 |
if retriever:
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
# 2. 呼叫 LLM 產生文字回應
|
| 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
|
| 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
|
| 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():
|