Spaces:
Sleeping
Sleeping
import os | |
import openai | |
import gradio as gr | |
import gspread | |
from google.oauth2.service_account import Credentials | |
from datetime import datetime | |
import plotly.express as px | |
# 從環境變數中取得 OpenAI API key | |
openai.api_key = os.environ.get("OenAI_API") | |
if not openai.api_key: | |
raise ValueError("OenAI_API not found. Please set it in your Hugging Face Space secret settings.") | |
# 讀取Google Service Account憑證檔案並授權 | |
creds = Credentials.from_service_account_file("service_account.json", scopes=["https://www.googleapis.com/auth/spreadsheets"]) | |
gc = gspread.authorize(creds) | |
# 請替換為您實際的 Google 試算表 URL | |
SHEET_URL = "https://docs.google.com/spreadsheets/d/1qS6EBih-icMAXoMLMNObFsT6-WJYD8fQPfN8xSRukPY/edit?usp=sharing" | |
sheet = gc.open_by_url(SHEET_URL).sheet1 | |
user_data = {} | |
mbti_questions = [ | |
{"question": "你在一個熱鬧的派對中,通常傾向於:", | |
"options": ["積極認識新朋友", "與少數熟識的人安靜交流"], | |
"dimension": "EI"}, | |
{"question": "週末時,你比較喜歡:", | |
"options": ["參加團體活動", "獨自從事個人嗜好"], | |
"dimension": "EI"}, | |
{"question": "當你獲得一項新計畫的資訊時,你通常會先:", | |
"options": ["注意可用的實際細節", "思考其未來可能的影響"], | |
"dimension": "SN"}, | |
{"question": "在學習新知識時,你比較傾向:", | |
"options": ["先掌握具體事實", "探索概念與模式"], | |
"dimension": "SN"}, | |
{"question": "在解決問題時,你較重視:", | |
"options": ["實際可行的解決方案", "創造性的替代方案"], | |
"dimension": "SN"}, | |
{"question": "當需要做決定時,你比較重視:", | |
"options": ["邏輯與客觀分析", "個人價值與感受"], | |
"dimension": "TF"}, | |
{"question": "當朋友向你求助,你更傾向於:", | |
"options": ["理性分析問題並給出建議", "先同理其感受並給予情緒支持"], | |
"dimension": "TF"}, | |
{"question": "在職場環境中,你通常更欣賞:", | |
"options": ["公平而合理的決策過程", "關懷並顧及員工感受的氛圍"], | |
"dimension": "TF"}, | |
{"question": "當規劃假期旅行時,你偏好:", | |
"options": ["提前制定詳細行程", "保持彈性隨機應變"], | |
"dimension": "JP"}, | |
{"question": "在處理日常事務時,你比較傾向:", | |
"options": ["結構化、按步驟進行", "即興而行、享受變化"], | |
"dimension": "JP"} | |
] | |
question_index = 0 | |
e_count, i_count = 0, 0 | |
s_count, n_count = 0, 0 | |
t_count, f_count = 0, 0 | |
j_count, p_count = 0, 0 | |
def gpt_recommendation(input_content, user_language): | |
if user_language == '繁體中文': | |
system_content = ( | |
"你是一位熟悉MBTI人格測驗和相關知識的酒吧助理," | |
"精通世界知名的經典調酒,且能推薦不同的經典調酒" | |
"推薦的經典調酒,一定要是真的存在於世界上" | |
"你只能回答關於「酒類、口味、MBTI人格」三大類問題與推薦" | |
"一定要快速介紹這個 MBTI 的人格特質,和適合的工作、適合朋友(列舉兩種MBTI性格)、適合戀愛對象(列舉兩種MBTI性格)" | |
"你也擅長數據分析,會將調酒內的各項成分比例清楚寫出來,請務必在推薦中,將每種成分的毫升數列成多行格式。例如:" | |
"- 琴酒:30毫升\n- 甜苦艾酒:30毫升\n- 金巴利:30毫升" | |
"專業且友善地推薦適合的調酒。" | |
"介紹這杯調酒的故事和口感,以及為什麼代表你的MBTI。" | |
"針對每個MBTI的結果,做個興趣分析(共5個),方便調酒師與客人能開啟聊天用。" | |
"並以繁體中文回應。" | |
) | |
else: | |
system_content = ( | |
"You are a bar assistant familiar with the MBTI personality test and related knowledge." | |
"You are proficient in world-renowned classic cocktails and can recommend different classic cocktails." | |
"The recommended cocktails must be real and exist in the world." | |
"You can only respond to three types of questions and recommendations: ‘alcohol, flavors, and MBTI personalities.‘" | |
"You are also skilled in data analysis and will clearly outline the ingredient ratios for each recommended cocktail. Please ensure to list the volume of each ingredient in milliliters in a multi-line format, for example:" | |
"- Gin: 30ml\n- Sweet Vermouth: 30ml\n- Campari: 30ml" | |
"Provide professional and friendly recommendations for suitable cocktails." | |
"For each MBTI result, conduct an interest analysis (a total of 5 points) to help bartenders and guests initiate conversations." | |
"Respond in English." | |
) | |
messages = [ | |
{"role": "system", "content": system_content}, | |
{"role": "user", "content": input_content} | |
] | |
try: | |
response = openai.ChatCompletion.create( | |
model="gpt-4o", | |
messages=messages, | |
temperature=0.7, | |
max_tokens=2048 | |
) | |
recommendation = response.choices[0].message['content'] | |
except Exception as e: | |
recommendation = f"調用 GPT-4o 失敗:{e}" if user_data.get('language', '繁體中文') == '繁體中文' else f"GPT-4 call failed: {e}" | |
return recommendation | |
def language_selection(language): | |
user_data['language'] = language | |
if language == '繁體中文': | |
return (gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(visible=True), | |
"請輸入您的手機號碼:", | |
"請輸入您的暱稱或姓名:", | |
gr.update(visible=False)) | |
else: | |
return (gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(visible=True), | |
"Please enter your phone number:", | |
"Please enter your nickname or name:", | |
gr.update(visible=False)) | |
def collect_user_info(phone, name): | |
user_data['phone'] = phone | |
user_data['name'] = name | |
if not phone or not name: | |
if user_data['language'] == '繁體中文': | |
return (gr.update(visible=True), "請輸入完整的手機號碼和暱稱或姓名。", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)) | |
else: | |
return (gr.update(visible=True), "Please enter both your phone number and nickname or name.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)) | |
options = ["我想要做個MBTI人格測驗","直接告訴您,我的MBTI人格,麻煩你推薦調酒給我","直接點酒,想喝的口感或風味?"] if user_data['language'] == '繁體中文' else [ | |
"I want to do an MBTI personality test", | |
"I will directly tell you my MBTI personality type, please recommend a cocktail to me", | |
"Order a drink directly, what taste or flavor do you want?" | |
] | |
return (gr.update(choices=options, visible=True), "", gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)) | |
def option_selection(selected_option): | |
user_data['selected_option'] = selected_option | |
if selected_option in ["我想要做個MBTI人格測驗", "I want to do an MBTI personality test"]: | |
return (f"測驗開始!請回答以下問題:\n{mbti_questions[0]['question']}", | |
gr.update(choices=mbti_questions[0]["options"], visible=True), | |
gr.update(visible=True), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=False)) | |
elif selected_option in ["直接告訴您,我的MBTI人格,麻煩你推薦調酒給我", "I will directly tell you my MBTI personality type, please recommend a cocktail to me"]: | |
prompt = "請輸入您的MBTI類型(例如:INTJ):" if user_data['language'] == '繁體中文' else "Please enter your MBTI type (e.g., INTJ):" | |
return (prompt, | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=True), # 顯示 MBTI類型輸入框 | |
gr.update(visible=False), | |
gr.update(visible=True), | |
gr.update(visible=False), | |
gr.update(visible=False)) | |
else: | |
prompt = "請在下方格子輸入想喝的口感或風味描述(例如:清爽的、柑橘味、微甜、酒感重)" if user_data['language'] == '繁體中文' else "Please enter the taste or flavor you would like (e.g., refreshing, citrus, slightly sweet):" | |
return (prompt, | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=True), | |
gr.update(visible=False), | |
gr.update(visible=True), | |
gr.update(visible=False)) | |
def mbti_test(answer): | |
global question_index, e_count, i_count, s_count, n_count, t_count, f_count, j_count, p_count | |
current_question = mbti_questions[question_index] | |
dim = current_question["dimension"] | |
index_chosen = current_question["options"].index(answer) | |
if dim == "EI": | |
e_count += (1 if index_chosen == 0 else 0) | |
i_count += (1 if index_chosen == 1 else 0) | |
elif dim == "SN": | |
s_count += (1 if index_chosen == 0 else 0) | |
n_count += (1 if index_chosen == 1 else 0) | |
elif dim == "TF": | |
t_count += (1 if index_chosen == 0 else 0) | |
f_count += (1 if index_chosen == 1 else 0) | |
elif dim == "JP": | |
j_count += (1 if index_chosen == 0 else 0) | |
p_count += (1 if index_chosen == 1 else 0) | |
question_index += 1 | |
if question_index < len(mbti_questions): | |
next_question = mbti_questions[question_index] | |
return (f"問題 {question_index + 1}/{len(mbti_questions)}:\n{next_question['question']}", | |
gr.update(choices=next_question["options"], visible=True), | |
gr.update(visible=False), | |
gr.update(visible=False), | |
gr.update(visible=True)) | |
else: | |
mbti_result = ( | |
("E" if e_count >= i_count else "I") + | |
("S" if s_count >= n_count else "N") + | |
("T" if t_count >= f_count else "F") + | |
("J" if j_count >= p_count else "P") | |
) | |
question_index = 0 | |
e_count = i_count = s_count = n_count = t_count = f_count = j_count = p_count = 0 | |
user_data["mbti_result"] = mbti_result | |
input_content = f"My MBTI type is {mbti_result}. Recommend a suitable cocktail..." | |
recommendation = gpt_recommendation(input_content, user_data['language']) | |
# 全部答題完成並產生推薦後,隱藏 btn_answer | |
return (f"測驗完成!您的 MBTI 類型為:{mbti_result}\n調酒推薦:\n{recommendation}\n\n請為MBTI準確度及AI推薦調酒進行評分(1到10分,最滿意為10分):", | |
gr.update(visible=False), | |
gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(visible=False)) | |
def direct_mbti_recommendation(mbti_type): | |
user_data["mbti_result"] = mbti_type.upper() | |
input_content = f"My MBTI type is {user_data['mbti_result']}. Recommend a suitable cocktail..." | |
recommendation = gpt_recommendation(input_content, user_data['language']) | |
return (f"您的 MBTI 類型為:{user_data['mbti_result']}\n調酒推薦:\n{recommendation}\n\n請為MBTI準確度及AI推薦調酒進行評分(1到10分,最滿意為10分):", | |
gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(visible=False)) | |
def flavor_recommendation(flavor): | |
user_data["flavor"] = flavor | |
input_content = f"I want a cocktail with a {flavor} flavor..." | |
recommendation = gpt_recommendation(input_content, user_data['language']) | |
return (f"您想要的口感是:{flavor}\n調酒推薦:\n{recommendation}\n\n請為MBTI準確度及AI推薦調酒進行評分(1到10分,最滿意為10分):", | |
gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(visible=False)) | |
def submit_rating(rating, assistant_text): | |
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
phone = user_data.get('phone', '') | |
name = user_data.get('name', '') | |
mbti_result = user_data.get('mbti_result', '') | |
sheet.append_row([now, phone, name, mbti_result, rating, assistant_text]) | |
if user_data['language'] == '繁體中文': | |
return "謝謝你給予寶貴的意見", gr.update(visible=False), gr.update(visible=False) | |
else: | |
return "Thanks for the feedback", gr.update(visible=False), gr.update(visible=False) | |
def parse_cocktail_ratios(assistant_text): | |
lines = assistant_text.split('\n') | |
ratios = {} | |
found = False | |
for line in lines: | |
line_lower = line.lower() | |
if ('毫升' in line_lower or '%' in line_lower or 'ml' in line_lower): | |
found = True | |
item = (line.strip() | |
.replace(':', ':') | |
.replace('毫升', '') | |
.replace('ml', '') | |
.replace('%', '')) | |
parts = item.split(':') | |
if len(parts) >= 2: | |
ingredient = parts[0].strip("- ").strip() | |
try: | |
ratio_value = float(parts[1].strip()) | |
if ratio_value <= 0: | |
continue | |
ratios[ingredient] = ratio_value | |
except: | |
pass | |
if not found or not ratios: | |
return None | |
return ratios | |
def generate_pie_chart(assistant_text): | |
ratios = parse_cocktail_ratios(assistant_text) | |
if not ratios: | |
fig = px.scatter(title="尚未偵測到正式的調酒配方比例") | |
return fig | |
ingredients = list(ratios.keys()) | |
values = list(ratios.values()) | |
fig = px.pie(values=values, names=ingredients, title="調酒配方圓餅圖") | |
return fig | |
with gr.Blocks() as demo: | |
language = gr.Radio(choices=["繁體中文", "English"], label="選擇介面語言 / Choose Interface Language") | |
btn_language = gr.Button("確認 / Confirm") | |
phone_input = gr.Textbox(label="", visible=False) | |
name_input = gr.Textbox(label="", visible=False) | |
btn_user_info = gr.Button("提交 / Submit", visible=False) | |
option = gr.Radio(choices=[], label="", visible=False) | |
btn_option = gr.Button("確認 / Confirm", visible=False) | |
# 使「酒吧助理回應」框互動為False,即唯讀不可輸入 | |
assistant_output = gr.Textbox(label="酒吧助理回應 / Assistant Response", visible=True, interactive=False) | |
answer = gr.Radio(choices=[], label="選擇答案 / Choose an Answer:", visible=False) | |
btn_answer = gr.Button("提交答案 / Submit Answer", visible=False) | |
mbti_input = gr.Textbox(label="請在下方輸入您的MBTI類型,大寫英文", visible=False) | |
btn_mbti_input = gr.Button("提交 MBTI 類型", visible=False) | |
flavor_input = gr.Textbox(label="請在下方輸入口感或風味描述", visible=False) | |
btn_flavor_input = gr.Button("提交口感或風味", visible=False) | |
rating = gr.Slider(minimum=1, maximum=10, step=1, label="請為MBTI準確度及AI推薦調酒進行評分(1到10分,最滿意為10分)", visible=False) | |
btn_rating_submit = gr.Button("送出", visible=False) | |
chart_output = gr.Plot(label="調酒配方圓餅圖", visible=True) | |
btn_language.click( | |
language_selection, | |
inputs=language, | |
outputs=[phone_input, name_input, btn_user_info, phone_input, name_input, btn_language] | |
) | |
btn_user_info.click( | |
collect_user_info, | |
inputs=[phone_input, name_input], | |
outputs=[option, assistant_output, phone_input, btn_option, btn_user_info] | |
) | |
btn_option.click( | |
option_selection, | |
inputs=option, | |
outputs=[assistant_output, answer, btn_answer, mbti_input, flavor_input, btn_mbti_input, btn_flavor_input, btn_option] | |
) | |
btn_answer.click( | |
mbti_test, | |
inputs=answer, | |
outputs=[assistant_output, answer, rating, btn_rating_submit, btn_answer] | |
) | |
btn_mbti_input.click( | |
direct_mbti_recommendation, | |
inputs=mbti_input, | |
outputs=[assistant_output, rating, btn_rating_submit, btn_mbti_input] | |
) | |
btn_flavor_input.click( | |
flavor_recommendation, | |
inputs=flavor_input, | |
outputs=[assistant_output, rating, btn_rating_submit, btn_flavor_input] | |
) | |
btn_rating_submit.click( | |
submit_rating, | |
inputs=[rating, assistant_output], | |
outputs=[assistant_output, rating, btn_rating_submit] | |
) | |
assistant_output.change( | |
fn=generate_pie_chart, | |
inputs=assistant_output, | |
outputs=chart_output | |
) | |
demo.launch(server_name="0.0.0.0", server_port=7860) |