bookMatch / app.py
youngtsai's picture
api_key = os.getenv("OPENAI_API_KEY")
82930d7
import gradio as gr
import json
import random
import os
from typing import List, Dict
import openai
# 設定 OpenAI API 金鑰
def load_api_key():
# 嘗試從環境變數獲取 API 金鑰
api_key = os.getenv("OPENAI_API_KEY")
# 如果環境變數中沒有 API 金鑰,嘗試從 credential.json 讀取
if not api_key:
try:
with open('credential.json', 'r', encoding='utf-8') as f:
credentials = json.load(f)
api_key = credentials.get("OPENAI_API_KEY")
except (FileNotFoundError, json.JSONDecodeError, KeyError) as e:
print(f"無法從 credential.json 讀取 API 金鑰: {e}")
return api_key
openai.api_key = load_api_key()
if not openai.api_key:
print("警告:未設定 OPENAI_API_KEY,請在環境變數或 credential.json 中設定")
# 載入書籍資料
def load_books() -> List[Dict]:
with open('data/books.json', 'r', encoding='utf-8') as f:
data = json.load(f)
return data['books']
books = load_books()
# 問題設定
QUESTIONS = {
"reading_goal": {
"question": "你現在最想達成什麼目標?",
"options": {
"提升工作效率": ["商業管理", "職涯發展"],
"自我成長": ["心理勵志", "溝通表達"],
"教育相關": ["教育"],
"技術創新": ["科技創新", "創新科技"],
"人文涵養": ["文學", "歷史", "歷史傳記", "哲學"]
}
},
"reading_time": {
"question": "你平均多久能看完一本書?",
"options": [
"1-2天",
"3-7天",
"2-3週",
"1個月以上"
]
},
"current_challenge": {
"question": "你目前面臨什麼挑戰?",
"options": [
"工作壓力大",
"想轉換跑道",
"需要新技能",
"尋找人生方向",
"情緒管理"
]
}
}
def get_book_recommendation(goal: str, reading_time: str, challenge: str, user_preferences=None) -> List[Dict]:
"""根據使用者輸入和聊天歷史推薦書籍"""
# 根據目標選擇對應類別
target_categories = QUESTIONS["reading_goal"]["options"][goal]
# 篩選符合類別的書籍
potential_books = [
book for book in books
if book['category'] in target_categories
]
# 如果找不到書,從所有書中選擇
if not potential_books:
potential_books = books
# 選擇3本書
if openai.api_key:
try:
# 使用 OpenAI API 來選擇最適合的書籍
book_info = []
for book in potential_books:
book_info.append(f"標題:{book['title']}, 作者:{book.get('author', '未知作者')}, 類別:{book['category']}, 簡介:{book.get('description', '無簡介')}")
# 如果有用戶偏好,將其納入 prompt 中
if user_preferences and openai.api_key:
# 修改 prompt,加入用戶偏好
prompt = f"""
作為一個專業的選書顧問,請根據以下用戶資訊,從提供的書籍清單中選出最適合的3本書:
用戶資訊:
- 閱讀目標:{goal}
- 閱讀時間:{reading_time}
- 目前面臨的挑戰:{challenge}
用戶偏好:
- 喜愛的作者:{', '.join(user_preferences.get('authors', []))}
- 喜愛的類型:{', '.join(user_preferences.get('genres', []))}
- 感興趣的主題:{', '.join(user_preferences.get('topics', []))}
- 喜歡的書籍:{', '.join(user_preferences.get('liked_books', []))}
- 不喜歡的書籍:{', '.join(user_preferences.get('disliked_books', []))}
可選書籍清單:
{book_info}
請選出3本最適合的書籍,並以JSON格式返回,格式如下:
{{
"recommendations": [
{{"title": "書名1"}},
{{"title": "書名2"}},
{{"title": "書名3"}}
]
}}
只返回JSON,不要有其他文字。
"""
else:
prompt = f"""
作為一個專業的選書顧問,請根據以下用戶資訊,從提供的書籍清單中選出最適合的3本書:
用戶資訊:
- 閱讀目標:{goal}
- 閱讀時間:{reading_time}
- 目前面臨的挑戰:{challenge}
可選書籍清單:
{book_info}
請選出3本最適合的書籍,並以JSON格式返回,格式如下:
{{
"recommendations": [
{{"title": "書名1"}},
{{"title": "書名2"}},
{{"title": "書名3"}}
]
}}
只返回JSON,不要有其他文字。
"""
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": "你是一個專業的選書顧問,根據用戶需求推薦最適合的書籍。"},
{"role": "user", "content": prompt}],
temperature=0.7
)
# 解析 AI 回應
try:
ai_response = response.choices[0].message.content
# 提取 JSON 部分
import re
json_match = re.search(r'({.*})', ai_response, re.DOTALL)
if json_match:
ai_response = json_match.group(1)
recommendations_data = json.loads(ai_response)
recommended_titles = [rec["title"] for rec in recommendations_data["recommendations"]]
# 找出對應的書籍完整資料
recommended = []
for title in recommended_titles:
for book in potential_books:
if book['title'] == title:
recommended.append(book)
break
# 如果 AI 推薦的書籍不足3本,從 potential_books 中隨機補充
if len(recommended) < 3 and len(potential_books) >= 3:
remaining_books = [b for b in potential_books if b not in recommended]
additional = random.sample(remaining_books, min(3 - len(recommended), len(remaining_books)))
recommended.extend(additional)
except (json.JSONDecodeError, KeyError, IndexError) as e:
print(f"解析 AI 回應時出錯: {e}")
# 如果解析失敗,使用隨機選擇
recommended = random.sample(potential_books, min(3, len(potential_books)))
except Exception as e:
print(f"OpenAI API 錯誤: {e}")
# 如果 API 呼叫失敗,使用隨機選擇
recommended = random.sample(potential_books, min(3, len(potential_books)))
else:
# 如果沒有 API 金鑰,使用隨機選擇
recommended = random.sample(potential_books, min(3, len(potential_books)))
# 生成推薦理由
for book in recommended:
book['recommendation_reason'] = generate_recommendation_reason(
book, goal, reading_time, challenge
)
return recommended
def generate_recommendation_reason(book: Dict, goal: str, reading_time: str, challenge: str) -> str:
"""生成客製化的推薦理由"""
if openai.api_key:
try:
prompt = f"""
請為以下書籍生成一段個性化的推薦理由,考慮用戶的閱讀目標、閱讀時間和當前面臨的挑戰:
書籍資訊:
- 標題:{book['title']}
- 作者:{book.get('author', '未知作者')}
- 類別:{book['category']}
- 簡介:{book.get('description', '無簡介')}
用戶資訊:
- 閱讀目標:{goal}
- 閱讀時間:{reading_time}
- 目前面臨的挑戰:{challenge}
請生成一段簡短但有說服力的推薦理由,包含以下幾點:
1. 為什麼這本書適合用戶的閱讀目標
2. 根據用戶的閱讀時間給出閱讀建議
3. 這本書如何幫助用戶應對當前的挑戰
4. 書中的一個關鍵洞見或亮點
使用友善、專業的語氣,並加入適當的表情符號增加親和力。
"""
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": "你是一個專業的書籍推薦專家,擅長為讀者找到最適合的書籍並提供個性化的推薦理由。"},
{"role": "user", "content": prompt}],
temperature=0.8,
max_tokens=300
)
return response.choices[0].message.content
except Exception as e:
print(f"生成推薦理由時出錯: {e}")
# 如果 API 呼叫失敗,使用基本推薦理由
return generate_basic_recommendation_reason(book, goal, reading_time, challenge)
else:
# 如果沒有 API 金鑰,使用基本推薦理由
return generate_basic_recommendation_reason(book, goal, reading_time, challenge)
def generate_basic_recommendation_reason(book: Dict, goal: str, reading_time: str, challenge: str) -> str:
"""生成基本的推薦理由(當 API 不可用時)"""
reasons = [
f"📚 這是一本{book['category']}類型的書籍",
f"👉 特別適合想要{goal}的讀者",
]
# 如果有作者資訊,加入作者
if 'author' in book:
reasons.insert(1, f"✍️ 作者:{book['author']}")
# 根據閱讀時間給建議
time_suggestions = {
"1-2天": "這本書結構清晰,適合快速閱讀",
"3-7天": "可以慢慢品味,每天讀一個章節",
"2-3週": "建議可以做筆記,深入思考",
"1個月以上": "這本書內容豐富,值得細細咀嚼"
}
reasons.append(f"⏰ {time_suggestions[reading_time]}")
# 根據當前挑戰提供建議
challenge_suggestions = {
"工作壓力大": "書中提供實用的壓力管理方法",
"想轉換跑道": "可以幫助你探索新的可能性",
"需要新技能": "提供紮實的知識基礎",
"尋找人生方向": "從中獲得人生啟發",
"情緒管理": "包含許多情緒調節的實用建議"
}
reasons.append(f"💡 {challenge_suggestions[challenge]}")
return "\n".join(reasons)
def recommend(goal: str, reading_time: str, challenge: str, chat_history=None) -> str:
"""主要推薦函數"""
# 從聊天歷史中提取用戶偏好
user_preferences = extract_preferences_from_chat(chat_history) if chat_history else None
recommendations = get_book_recommendation(goal, reading_time, challenge, user_preferences)
output = "🎯 為你找到以下推薦書籍:\n\n"
# 如果有從對話中提取的偏好,先顯示這些洞察
if user_preferences:
output += "📝 根據我們的對話,我注意到:\n"
if user_preferences.get('authors') and len(user_preferences['authors']) > 0:
output += f"- 你喜歡的作者:{', '.join(user_preferences['authors'])}\n"
if user_preferences.get('genres') and len(user_preferences['genres']) > 0:
output += f"- 你偏好的類型:{', '.join(user_preferences['genres'])}\n"
if user_preferences.get('topics') and len(user_preferences['topics']) > 0:
output += f"- 你感興趣的主題:{', '.join(user_preferences['topics'])}\n"
if user_preferences.get('liked_books') and len(user_preferences['liked_books']) > 0:
output += f"- 你喜歡的書籍:{', '.join(user_preferences['liked_books'])}\n"
if user_preferences.get('disliked_books') and len(user_preferences['disliked_books']) > 0:
output += f"- 你不喜歡的書籍:{', '.join(user_preferences['disliked_books'])}\n"
output += "\n我根據這些偏好和你的回答為你推薦以下書籍:\n\n"
for i, book in enumerate(recommendations, 1):
output += f"📖 推薦書籍 {i}:《{book['title']}》\n\n"
output += f"{book['recommendation_reason']}\n"
output += "-------------------\n\n"
return output
def extract_preferences_from_chat(chat_history):
"""從聊天歷史中提取用戶偏好"""
if not chat_history or not openai.api_key:
return None
try:
# 將聊天歷史轉換為文本
chat_text = ""
for exchange in chat_history:
if len(exchange) >= 2: # 確保有用戶訊息和回應
chat_text += f"用戶: {exchange[0]}\n助手: {exchange[1]}\n\n"
# 使用 OpenAI 分析聊天內容,提取用戶偏好
prompt = f"""
請分析以下聊天記錄,提取用戶的閱讀偏好。關注以下幾點:
1. 喜愛的作者
2. 喜愛的書籍類型/類別
3. 感興趣的主題
4. 明確提到喜歡的書籍
5. 明確提到不喜歡的書籍
聊天記錄:
{chat_text}
請以JSON格式返回結果,格式如下:
{{
"authors": ["作者1", "作者2"],
"genres": ["類型1", "類型2"],
"topics": ["主題1", "主題2"],
"liked_books": ["書名1", "書名2"],
"disliked_books": ["書名1", "書名2"]
}}
如果某個類別沒有找到相關信息,請提供空列表。
只返回JSON,不要有其他文字。
"""
response = openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一個專業的數據分析師,專門從對話中提取用戶偏好。"},
{"role": "user", "content": prompt}
],
temperature=0.3
)
# 解析 AI 回應
ai_response = response.choices[0].message.content
# 提取 JSON 部分
import re
json_match = re.search(r'({.*})', ai_response, re.DOTALL)
if json_match:
ai_response = json_match.group(1)
preferences = json.loads(ai_response)
return preferences
except Exception as e:
print(f"從聊天歷史提取偏好時出錯: {e}")
return None
# 聊天機器人相關函數
def chatbot_response(message, history, goal, reading_time, challenge):
"""處理聊天機器人的回應"""
# 確保 history 是一個列表
if history is None:
history = []
# 初始化新的歷史記錄
new_history = history.copy()
# 如果有 OpenAI API 金鑰,使用 OpenAI 生成回應
if openai.api_key:
try:
# 構建對話歷史
conversation = []
for h in history:
conversation.append({"role": "user", "content": h[0]})
conversation.append({"role": "assistant", "content": h[1]})
# 添加當前用戶訊息
conversation.append({"role": "user", "content": message})
# 添加系統提示,讓 AI 更有個性和智能
system_prompt = f"""你是一個專業且充滿個性的選書助手,名叫「書蟲智慧」。
關於用戶的資訊:
- 閱讀目標:{goal}
- 閱讀時間:{reading_time}
- 目前面臨的挑戰:{challenge}
你的任務:
1. 提供個性化的書籍推薦
2. 分析用戶的閱讀偏好和習慣
3. 主動提問,引導用戶發現新的閱讀興趣
4. 分享閱讀技巧和書籍相關知識
5. 使用生動活潑的語言,展現你的書蟲個性
你可以:
- 詢問用戶喜歡的作者或書籍類型
- 分享書籍的有趣知識和背景
- 根據用戶的回答調整推薦
- 提供閱讀建議和技巧
回應應該友善、專業、有趣,並使用繁體中文。如果是初次對話,請自我介紹並詢問用戶的閱讀偏好。"""
conversation.insert(0, {"role": "system", "content": system_prompt})
# 呼叫 OpenAI API
api_response = openai.chat.completions.create(
model="gpt-4o", # 使用更強大的模型
messages=conversation,
temperature=0.8, # 增加創意性
max_tokens=800 # 允許更長的回應
)
response = api_response.choices[0].message.content
new_history.append([message, response])
except Exception as e:
print(f"OpenAI API 錯誤:{e}")
# 如果 API 呼叫失敗,使用基本回應
new_history.append([message, "抱歉,我目前無法連接到我的知識庫。請稍後再試,或者告訴我你喜歡什麼類型的書籍,我會嘗試提供一些基本建議。"])
else:
# 如果沒有 API 金鑰,使用基本回應
new_history.append([message, "我需要連接到我的知識庫才能提供更智能的回應。請確保設置了正確的 API 金鑰。不過,你可以告訴我你喜歡什麼類型的書籍,我會嘗試提供一些基本建議。"])
return new_history
# 創建Gradio界面
with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important;}") as demo:
gr.Markdown("# 📚 選書引導師 BookMatch AI")
gr.Markdown("## 回答三個簡單問題,讓AI為您推薦最適合的書籍")
with gr.Row():
with gr.Column(scale=1):
goal = gr.Dropdown(
choices=list(QUESTIONS["reading_goal"]["options"].keys()),
label=QUESTIONS["reading_goal"]["question"],
info="選擇你的閱讀目標"
)
reading_time = gr.Radio(
choices=QUESTIONS["reading_time"]["options"],
label=QUESTIONS["reading_time"]["question"],
info="這能幫助我們推薦適合的閱讀材料"
)
challenge = gr.Radio(
choices=QUESTIONS["current_challenge"]["options"],
label=QUESTIONS["current_challenge"]["question"],
info="讓我們了解你的需求"
)
gr.Markdown("## 💬 與選書助手對話")
gr.Markdown("告訴我你讀過哪些書,喜歡什麼類型,我可以給你更個性化的推薦")
chatbot = gr.Chatbot(height=300)
msg = gr.Textbox(label="輸入訊息")
with gr.Column(scale=1):
submit_btn = gr.Button("獲取推薦", variant="primary")
recommendation_output = gr.Textbox(
label="推薦結果",
lines=20
)
submit_btn.click(
fn=recommend,
inputs=[goal, reading_time, challenge, chatbot],
outputs=recommendation_output
)
msg.submit(
fn=chatbot_response,
inputs=[msg, chatbot, goal, reading_time, challenge],
outputs=chatbot
).then(
lambda x: "",
inputs=msg,
outputs=msg
)
if __name__ == "__main__":
demo.launch(share=True)