ciecietaipei-chatbot / src /streamlit_app.py
DeepLearning101's picture
Update src/streamlit_app.py
14c487c verified
import streamlit as st
from google import genai
from google.genai import types # 引入 types 用來設定對話歷史與系統指令
import requests
import os
import time # 記得在程式碼最上方加入這個內建套件(如果還沒加的話)
# 0. 頁面配置與 CSS 注入(隱藏側邊欄捲軸)
st.set_page_config(page_title="CIE CIE TAIPEI 線上客服", page_icon="🍸", layout="wide")
st.markdown(
"""
<style>
/* 1. 隱藏側邊欄捲軸 */
[data-testid="stSidebar"] section::-webkit-scrollbar { display: none; }
[data-testid="stSidebar"] section { -ms-overflow-style: none; scrollbar-width: none; }
/* 🚀 2. 全域字體縮小,適應手機螢幕 */
html, body, [class*="st-"] {
font-size: 14px !important;
}
/* 🚀 3. 調整範例按鈕樣式:變矮、字體變小、支援多行自動折行 */
.stButton button {
width: 100%;
padding: 6px 10px; /* 縮小上下內距 */
border-radius: 12px;
border: 1px solid #D4AF37;
color: #D4AF37;
background-color: transparent;
font-size: 13px !important; /* 讓按鈕字體再小一點 */
line-height: 1.3;
min-height: auto;
white-space: normal; /* 允許長句子自動折行,避免撐爆寬度 */
height: auto;
}
/* 4. 極度壓縮主畫面空白 */
.block-container {
padding-top: 1.2rem !important;
padding-bottom: 5rem !important;
padding-left: 0.8rem !important;
padding-right: 0.8rem !important;
}
/* 5. 隱藏 Header 背景與右側選單,但保留左側展開按鈕 */
header[data-testid="stHeader"] {
background-color: rgba(0,0,0,0) !important;
}
header[data-testid="stHeader"] #MainMenu {
visibility: hidden;
}
div[data-testid="collapsedControl"] {
visibility: visible !important;
background-color: #1A1A1A !important;
color: #D4AF37 !important;
border-radius: 0 5px 5px 0;
top: 5px;
}
</style>
""",
unsafe_allow_html=True
)
# 從環境變數中取得 API Key
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
st.error("請確認已經在 Space 的 Settings 設定了 GEMINI_API_KEY")
st.stop()
# 【更新】使用新版 SDK 建立 Client
client = genai.Client(api_key=api_key)
# 1. 基礎設定與連結
# ⚠️ 注意:這裡預設你把 md 檔放在 github 的 ai-knowledge 資料夾下。如果是放在根目錄,請把 ai-knowledge/ 刪除。
MD_BASE_URL = "https://raw.githubusercontent.com/ciecietaipei/ciecietaipei.github.io/main/ai-knowledge/"
# 從你們的 assets 資料夾抓取 Logo
LOGO_URL = "https://raw.githubusercontent.com/ciecietaipei/ciecietaipei.github.io/main/assets/ciecie_logo_circle.png"
HOME_URL = "https://ciecietaipei.github.io"
KNOWLEDGE_MAP = {
"🍸 原創調酒 (Cocktails)": {
"raw_url": f"{MD_BASE_URL}cocktails.md",
"page_url": f"{HOME_URL}/cocktails.html",
"repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/cocktails.md"
},
"🍷 經典酒水 (Drinks)": {
"raw_url": f"{MD_BASE_URL}drinks.md",
"page_url": f"{HOME_URL}/drinks.html",
"repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/drinks.md"
},
"🍽️ 精緻餐點 (Foods)": {
"raw_url": f"{MD_BASE_URL}foods.md",
"page_url": f"{HOME_URL}/foods.html",
"repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/foods.md"
},
"🛋️ 空間環境 (Environment)": {
"raw_url": f"{MD_BASE_URL}environment.md",
"page_url": f"{HOME_URL}/environment.html",
"repo_url": "https://github.com/ciecietaipei/ciecietaipei.github.io/blob/main/ai-knowledge/environment.md"
}
}
# 2. 批量抓取內容
def fetch_all_knowledge():
combined_knowledge = ""
with st.spinner("正在同步 CIE CIE 最新菜單與資訊..."):
for category, info in KNOWLEDGE_MAP.items():
try:
response = requests.get(info["raw_url"])
response.raise_for_status()
combined_knowledge += f"\n\n## 【領域:{category}】\n"
combined_knowledge += response.text
except Exception as e:
st.warning(f"無法同步 {category} 的資料:請確認 GitHub 路徑是否正確。錯誤訊息:{e}")
return combined_knowledge
# 初始化 Session State
if "knowledge" not in st.session_state:
st.session_state.knowledge = fetch_all_knowledge()
if "messages" not in st.session_state:
st.session_state.messages = []
if "example_prompt" not in st.session_state:
st.session_state.example_prompt = None
# 3. 側邊欄設計
with st.sidebar:
# 放入 CIE CIE 的圓形 Logo
st.markdown(f'<a href="{HOME_URL}" target="_blank"><img src="{LOGO_URL}" width="100%" style="border-radius:50%; margin-bottom:20px;"></a>', unsafe_allow_html=True)
st.title("⚙️ 知識庫狀態")
for category, info in KNOWLEDGE_MAP.items():
with st.expander(category):
st.markdown(f"🔗 [瀏覽網頁]({info['page_url']})")
st.markdown(f"📂 [GitHub 原始碼]({info['repo_url']})")
st.markdown("---")
if st.button("🔄 手動更新菜單資訊"):
st.session_state.knowledge = fetch_all_knowledge()
st.success("資料已重新抓取!")
# 4. 主介面與範例問句
st.title("🍸 CIECIE 微醺客服")
st.caption("關於酒單、餐點、環境或訂位資訊與規範,歡迎直接詢問!台北市大安區信義路四段390號;02-2709-3446")
# 顯示專屬範例問句按鈕
example_cols = st.columns(6)
examples = [
"🍸 有推薦的『茶香』或『花果系』原創調酒嗎?",
"🍜 深夜想吃點熱的主食,聽說有厲害的餐點?",
"🛋️ 幫朋友慶生,請問二樓的沙發區環境如何?",
"🪵 想獨飲,推薦一杯酒感重、帶木質調的大人味特調?",
"🥃 請問你們有供應單一麥芽威士忌的單杯純飲嗎?",
"🍤 有什麼推薦適合四個人的下酒菜分享?"
]
for col, ex in zip(example_cols, examples):
if col.button(ex):
st.session_state.example_prompt = ex
# 5. 【更新】模型回覆邏輯(加入防呆重試機制 + 標示參考酒單/菜單)
def get_gemini_response(user_input):
system_instruction = f"""
你現在是位於台北市大安區信義安和的微醺酒吧『CIE CIE TAIPEI』的專業線上客服小助手。
你的說話風格熱情、有質感且帶有一點輕鬆微醺的氛圍。
以下是從官方網站最新同步的資訊(包含原創調酒、經典酒水、精緻餐點、環境與規範):
---
{st.session_state.knowledge}
---
【核心任務】:
請嚴格基於上述提供的資訊來回答客人的問題。
如果客人問了菜單上沒有的品項,或超出了上述資訊範圍,請禮貌地告知:「目前小助手手邊沒有這項資訊,建議您直接私訊我們的 IG 或是現場詢問調酒師喔!」
【嚴格輸出限制】:
1. 在回覆的最一開始,請務必先明確標示你是參考了文本中的哪個分類來作答(請擷取對應的【領域:XXX】標題)。例如:「🍸 **為您翻閱:【領域:🍸 原創調酒 (Cocktails)】**」,換行後再開始熱情地回答。
2. 絕對禁止輸出任何你的內部思考過程、計畫草稿、英文標籤、或是角色設定分析。
3. 所有的輸出內容必須 100% 都是直接面對客人溝通的繁體中文對話。
"""
# 加入自動重試迴圈,最多嘗試 2 次
max_retries = 2
for attempt in range(max_retries):
try:
# 將 Streamlit 儲存的歷史紀錄,轉換為新版 SDK 規定的格式
history_format = []
for msg in st.session_state.messages:
role = "user" if msg["role"] == "user" else "model"
history_format.append(
types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])])
)
# 使用新版 client 建立對話,並帶入歷史紀錄
chat = client.chats.create(
model="gemma-4-31b-it",
config=types.GenerateContentConfig(
system_instruction=system_instruction,
temperature=0.4, # 稍微帶點彈性,讓微醺語氣更生動自然
),
history=history_format
)
response = chat.send_message(user_input)
return response.text
except Exception as e:
error_msg = str(e)
# 遇到 500 錯誤時,如果在重試次數內,暫停 1.5 秒後自動重試
if "500" in error_msg and attempt < (max_retries - 1):
time.sleep(5)
continue
# 錯誤處理:根據酒吧氛圍客製化錯誤訊息
if "429" in error_msg or "quota" in error_msg.lower():
return "⚠️ **系統提示:**\n\n目前吧台前詢問的客人較多,小助手稍微忙不過來了!請您先喝杯水,稍等幾分鐘後再試喔!"
elif "500" in error_msg:
return "🔄 **系統提示:**\n\n小助手剛剛去倒了杯酒(連線短暫不穩定),請您再點擊發送一次剛剛的問題喔!"
else:
return f"❌ **發生預期外錯誤**\n\n訊息:{error_msg}"
# 6. 對話邏輯
prompt = st.chat_input("想喝點什麼?或是想了解什麼資訊呢?")
if st.session_state.example_prompt:
prompt = st.session_state.example_prompt
st.session_state.example_prompt = None
if prompt:
st.session_state.messages.append({"role": "user", "content": prompt})
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
with st.chat_message("assistant"):
response_text = get_gemini_response(prompt)
st.markdown(response_text)
st.session_state.messages.append({"role": "assistant", "content": response_text})
st.rerun()
else:
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])