|
import gradio as gr |
|
import json |
|
import random |
|
import os |
|
from typing import List, Dict |
|
import openai |
|
|
|
|
|
def load_api_key(): |
|
|
|
api_key = os.getenv("OPENAI_API_KEY") |
|
|
|
|
|
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 |
|
|
|
|
|
if openai.api_key: |
|
try: |
|
|
|
book_info = [] |
|
for book in potential_books: |
|
book_info.append(f"標題:{book['title']}, 作者:{book.get('author', '未知作者')}, 類別:{book['category']}, 簡介:{book.get('description', '無簡介')}") |
|
|
|
|
|
if user_preferences and openai.api_key: |
|
|
|
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 |
|
) |
|
|
|
|
|
try: |
|
ai_response = response.choices[0].message.content |
|
|
|
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 |
|
|
|
|
|
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}") |
|
|
|
recommended = random.sample(potential_books, min(3, len(potential_books))) |
|
else: |
|
|
|
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}") |
|
|
|
return generate_basic_recommendation_reason(book, goal, reading_time, challenge) |
|
else: |
|
|
|
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" |
|
|
|
|
|
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_response = response.choices[0].message.content |
|
|
|
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): |
|
"""處理聊天機器人的回應""" |
|
|
|
if history is None: |
|
history = [] |
|
|
|
|
|
new_history = history.copy() |
|
|
|
|
|
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}) |
|
|
|
|
|
system_prompt = f"""你是一個專業且充滿個性的選書助手,名叫「書蟲智慧」。 |
|
|
|
關於用戶的資訊: |
|
- 閱讀目標:{goal} |
|
- 閱讀時間:{reading_time} |
|
- 目前面臨的挑戰:{challenge} |
|
|
|
你的任務: |
|
1. 提供個性化的書籍推薦 |
|
2. 分析用戶的閱讀偏好和習慣 |
|
3. 主動提問,引導用戶發現新的閱讀興趣 |
|
4. 分享閱讀技巧和書籍相關知識 |
|
5. 使用生動活潑的語言,展現你的書蟲個性 |
|
|
|
你可以: |
|
- 詢問用戶喜歡的作者或書籍類型 |
|
- 分享書籍的有趣知識和背景 |
|
- 根據用戶的回答調整推薦 |
|
- 提供閱讀建議和技巧 |
|
|
|
回應應該友善、專業、有趣,並使用繁體中文。如果是初次對話,請自我介紹並詢問用戶的閱讀偏好。""" |
|
|
|
conversation.insert(0, {"role": "system", "content": system_prompt}) |
|
|
|
|
|
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}") |
|
|
|
new_history.append([message, "抱歉,我目前無法連接到我的知識庫。請稍後再試,或者告訴我你喜歡什麼類型的書籍,我會嘗試提供一些基本建議。"]) |
|
else: |
|
|
|
new_history.append([message, "我需要連接到我的知識庫才能提供更智能的回應。請確保設置了正確的 API 金鑰。不過,你可以告訴我你喜歡什麼類型的書籍,我會嘗試提供一些基本建議。"]) |
|
|
|
return new_history |
|
|
|
|
|
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) |