File size: 9,853 Bytes
8c9a5de
 
 
 
 
 
 
 
 
 
 
 
 
0d1630d
 
8c9a5de
 
0d1630d
8c9a5de
 
0d1630d
8c9a5de
 
 
 
0d1630d
8c9a5de
 
 
 
 
0d1630d
 
 
8c9a5de
 
0d1630d
8c9a5de
0d1630d
 
 
 
8c9a5de
 
 
 
0d1630d
8c9a5de
0d1630d
 
 
 
 
 
8c9a5de
 
0d1630d
8c9a5de
0d1630d
 
 
 
 
 
8c9a5de
0d1630d
 
 
 
 
 
 
8c9a5de
 
 
0d1630d
8c9a5de
0d1630d
8c9a5de
 
 
0d1630d
 
 
 
 
 
8c9a5de
 
 
 
 
 
 
0d1630d
8c9a5de
0d1630d
8c9a5de
 
0d1630d
8c9a5de
 
0d1630d
 
 
 
 
8c9a5de
0d1630d
 
8c9a5de
0d1630d
 
 
8c9a5de
 
 
 
0d1630d
8c9a5de
0d1630d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c9a5de
0d1630d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c9a5de
 
 
0d1630d
 
8c9a5de
 
 
0d1630d
 
 
8c9a5de
 
 
0d1630d
 
 
 
 
 
 
 
 
 
 
 
8c9a5de
 
 
0d1630d
 
 
 
8c9a5de
0d1630d
 
 
 
 
 
 
 
 
 
 
 
8c9a5de
0d1630d
8c9a5de
 
 
 
 
0d1630d
8c9a5de
 
0d1630d
 
8c9a5de
 
 
0d1630d
 
 
8c9a5de
0d1630d
8c9a5de
 
0d1630d
8c9a5de
 
0d1630d
 
 
 
8c9a5de
 
 
 
 
0d1630d
 
 
8c9a5de
0d1630d
8c9a5de
 
 
 
 
 
 
 
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
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

# --- [新增] 匯入組員的模組 ---
import teammate_logic 
# ---------------------------

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. 核心功能
# ==========================================

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

def generate_content_with_groq(restaurant_name, restaurant_detail, user_diary, mood_score, 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 內容
    user_msg = f"""
    【狀態】心情分數:{mood_score}
    【日記】{user_diary}
    
    【心情美食指南建議】
    因為分數是 {mood_score},建議吃這類食物的原因是:「{mood_guide_reason}」。
    
    【推薦餐廳】
    名稱:{restaurant_name}
    資料:{restaurant_detail}
    
    任務:
    請用繁體中文寫一段溫暖有趣的回覆:
    1. 綜合回應他的日記與心情。
    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

def mood_agent_logic(score_input, food_input, diary_input, 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])

    # 呼叫生成函式 (已移除 quiz_state 參數)
    ai_text = generate_content_with_groq(name, rag_info, diary_input, score_input, 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

# ==========================================
# 3. Gradio 介面建構
# ==========================================

# 結合組員的 CSS
combined_css = teammate_logic.css 

with gr.Blocks(title="AI 心情食堂", css=combined_css) as demo:
    
    with gr.Tabs():
        # Tab 1: 主功能區 (已移除 Q1-Q5,直接顯示點餐介面)
        with gr.TabItem("🍽️ AI 心情食堂 (點餐推薦)", id=0):
            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="地圖導航")

        # Tab 2: 組員功能
        with gr.TabItem("情緒辨識 (組員功能)", id=1):
            teammate_logic.render_ui()  # 呼叫組員的介面渲染函式

    # 事件綁定 (只保留主功能)
    submit_btn.click(
        fn=mood_agent_logic,
        inputs=[score_input, food_input, diary_input, debug_mode_btn],
        outputs=[agent_output, image_output, map_output]
    )

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