File size: 20,229 Bytes
d2468c1
 
 
 
 
 
 
 
 
 
82930d7
d2468c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
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)