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)